Spring BootOAuth2 Login
Spring Boot

OAuth2 Login

OAuth2 Login (also called Social Login) lets users sign in with an external identity provider — Google, GitHub, Facebook, Keycloak, or any OpenID Connect provider — instead of managing usernames and passwords. Spring Boot's spring-boot-starter-oauth2-client auto-configures the Authorization Code flow, handles redirects, token exchange, and user info fetching with minimal configuration.

OAuth2 Login Setup

Adding spring-boot-starter-oauth2-client and registering at least one OAuth2 provider in application.yml activates the full Authorization Code flow automatically. Spring Security handles the redirect to the provider, receives the authorization code, exchanges it for tokens, fetches user info, and creates an authenticated session.
XML
<!-- pom.xml: -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

# ── application.yml — register providers: ────────────────────────────
spring:
  security:
    oauth2:
      client:
        registration:
          # ── Google: ──────────────────────────────────────────────────
          google:
            client-id: ${GOOGLE_CLIENT_ID}
            client-secret: ${GOOGLE_CLIENT_SECRET}
            scope:
              - openid
              - profile
              - email

          # ── GitHub: ──────────────────────────────────────────────────
          github:
            client-id: ${GITHUB_CLIENT_ID}
            client-secret: ${GITHUB_CLIENT_SECRET}
            scope:
              - read:user
              - user:email

          # ── Facebook: ────────────────────────────────────────────────
          facebook:
            client-id: ${FACEBOOK_CLIENT_ID}
            client-secret: ${FACEBOOK_CLIENT_SECRET}
            scope:
              - email
              - public_profile

# Spring Boot provides built-in provider metadata for:
# google, github, facebook, okta
# No provider block needed for these — auto-discovered.

# ── What Spring Security configures automatically: ────────────────────
# GET  /oauth2/authorization/{registrationId}  — initiates the flow
#      e.g. /oauth2/authorization/google
# GET  /login/oauth2/code/{registrationId}      — receives the callback
#      e.g. /login/oauth2/code/google
# GET  /login                                   — shows provider links
# POST /logout                                  — invalidates session

Security Configuration for OAuth2 Login

The SecurityFilterChain configures which URLs are public, where to redirect after login, and how to handle failures. For server-side rendered apps this is straightforward; for SPAs the configuration requires a custom success handler that returns JSON instead of redirecting.
Java
@Configuration
@EnableWebSecurity
public class OAuth2LoginSecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/", "/login", "/css/**", "/js/**",
                                 "/error").permitAll()
                .anyRequest().authenticated()
            )
            .oauth2Login(oauth2 -> oauth2
                .loginPage("/login")                    // custom login page
                .defaultSuccessUrl("/dashboard", true)  // redirect after success
                .failureUrl("/login?error=true")        // redirect on failure
                .userInfoEndpoint(userInfo -> userInfo
                    .userService(customOAuth2UserService()) // custom user loading
                )
            )
            .logout(logout -> logout
                .logoutSuccessUrl("/")
                .invalidateHttpSession(true)
                .deleteCookies("JSESSIONID")
            );
        return http.build();
    }

    // ── Custom login page controller: ─────────────────────────────────
    // @GetMapping("/login")
    // returns "login" — template with links to each provider:
    // <a th:href="@{/oauth2/authorization/google}">Sign in with Google</a>
    // <a th:href="@{/oauth2/authorization/github}">Sign in with GitHub</a>
}

// ── For REST APIs / SPAs — JSON responses instead of redirects: ───────
@Bean
public SecurityFilterChain spaFilterChain(HttpSecurity http) throws Exception {
    http
        .oauth2Login(oauth2 -> oauth2
            .successHandler((request, response, authentication) -> {
                // Return JWT or session token as JSON:
                OAuth2User user = (OAuth2User) authentication.getPrincipal();
                String token = jwtService.generateToken(user.getName());
                response.setContentType(MediaType.APPLICATION_JSON_VALUE);
                response.getWriter().write(
                    "{"token": "" + token + "", " +
                    ""email": "" + user.getAttribute("email") + ""}");
            })
            .failureHandler((request, response, exception) -> {
                response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
                response.setContentType(MediaType.APPLICATION_JSON_VALUE);
                response.getWriter().write(
                    "{"error": "OAuth2 authentication failed: " +
                    exception.getMessage() + ""}");
            })
        )
        .sessionManagement(session -> session
            .sessionCreationPolicy(SessionCreationPolicy.STATELESS))
        .csrf(csrf -> csrf.disable());
    return http.build();
}

Custom OAuth2UserService — Linking to Local Users

The default OAuth2UserService loads user attributes from the provider's user info endpoint. A custom implementation allows you to create or update a local user record on first login, merge attributes from multiple providers, and assign application-specific roles.
Java
// ── Custom OAuth2UserService: ─────────────────────────────────────────
@Service
@RequiredArgsConstructor
public class CustomOAuth2UserService
        extends DefaultOAuth2UserService {

    private final UserRepository userRepository;

    @Override
    @Transactional
    public OAuth2User loadUser(OAuth2UserRequest request)
            throws OAuth2AuthenticationException {

        // Load user attributes from the provider:
        OAuth2User oAuth2User = super.loadUser(request);

        String provider = request.getClientRegistration()
            .getRegistrationId();                      // "google", "github"
        String providerId = oAuth2User.getName();      // provider's user ID
        String email      = extractEmail(oAuth2User, provider);
        String name       = extractName(oAuth2User, provider);

        // Find or create the local user:
        User user = userRepository
            .findByProviderAndProviderId(provider, providerId)
            .orElseGet(() -> userRepository
                .findByEmail(email)
                .map(existing -> {
                    // Link existing account to this provider:
                    existing.setProvider(provider);
                    existing.setProviderId(providerId);
                    return existing;
                })
                .orElseGet(() -> createNewUser(
                    provider, providerId, email, name)));

        // Update last login and name:
        user.setName(name);
        user.setLastLoginAt(LocalDateTime.now());
        userRepository.save(user);

        // Return a CustomOAuth2User that carries the local user:
        return new CustomOAuth2User(oAuth2User, user);
    }

    private User createNewUser(String provider, String providerId,
                                String email, String name) {
        User user = new User();
        user.setProvider(provider);
        user.setProviderId(providerId);
        user.setEmail(email);
        user.setName(name);
        user.setRoles(Set.of("USER"));
        user.setEnabled(true);
        return userRepository.save(user);
    }

    private String extractEmail(OAuth2User user, String provider) {
        return switch (provider) {
            case "google"   -> user.getAttribute("email");
            case "github"   -> user.getAttribute("email");
            case "facebook" -> user.getAttribute("email");
            default         -> user.getAttribute("email");
        };
    }

    private String extractName(OAuth2User user, String provider) {
        return switch (provider) {
            case "google"   -> user.getAttribute("name");
            case "github"   -> user.getAttribute("login");
            case "facebook" -> user.getAttribute("name");
            default         -> user.getAttribute("name");
        };
    }
}

// ── Custom OAuth2User — wraps the provider user + local user: ─────────
public class CustomOAuth2User implements OAuth2User {

    private final OAuth2User delegate;
    private final User localUser;

    public CustomOAuth2User(OAuth2User delegate, User localUser) {
        this.delegate  = delegate;
        this.localUser = localUser;
    }

    @Override
    public Map<String, Object> getAttributes() {
        return delegate.getAttributes();
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        // Use local user's roles, not provider's:
        return localUser.getRoles().stream()
            .map(role -> new SimpleGrantedAuthority("ROLE_" + role))
            .toList();
    }

    @Override
    public String getName() {
        return localUser.getId().toString();
    }

    public User getLocalUser() { return localUser; }
    public Long getUserId()    { return localUser.getId(); }
    public String getEmail()   { return localUser.getEmail(); }
}

Custom Provider — Keycloak and OpenID Connect

Any OpenID Connect provider can be registered by providing the issuer URI. Spring Security auto-discovers all endpoints from the OIDC well-known configuration URL. Keycloak is the most common self-hosted provider in enterprise Spring Boot applications.
yaml
# ── application.yml — Keycloak (OpenID Connect): ────────────────────
spring:
  security:
    oauth2:
      client:
        registration:
          keycloak:
            client-id: myapp
            client-secret: ${KEYCLOAK_CLIENT_SECRET}
            authorization-grant-type: authorization_code
            redirect-uri: "{baseUrl}/login/oauth2/code/{registrationId}"
            scope:
              - openid
              - profile
              - email
              - roles
        provider:
          keycloak:
            issuer-uri: https://keycloak.example.com/realms/myrealm
            # Spring Security discovers all endpoints from:
            # https://keycloak.example.com/realms/myrealm/.well-known/openid-configuration

# ── Custom Okta provider: ─────────────────────────────────────────────
spring:
  security:
    oauth2:
      client:
        registration:
          okta:
            client-id: ${OKTA_CLIENT_ID}
            client-secret: ${OKTA_CLIENT_SECRET}
            scope: openid,profile,email
        provider:
          okta:
            issuer-uri: https://dev-12345.okta.com/oauth2/default

# ── Azure AD: ─────────────────────────────────────────────────────────
spring:
  security:
    oauth2:
      client:
        registration:
          azure:
            client-id: ${AZURE_CLIENT_ID}
            client-secret: ${AZURE_CLIENT_SECRET}
            scope: openid,profile,email
            authorization-grant-type: authorization_code
        provider:
          azure:
            issuer-uri: >-
              https://login.microsoftonline.com/${AZURE_TENANT_ID}/v2.0

// ── Extract Keycloak roles from OIDC token: ────────────────────────────
@Service
public class KeycloakOidcUserService
        extends OidcUserService {

    @Override
    @Transactional
    public OidcUser loadUser(OidcUserRequest request)
            throws OAuth2AuthenticationException {

        OidcUser oidcUser = super.loadUser(request);
        OidcIdToken idToken = oidcUser.getIdToken();

        // Extract roles from Keycloak's realm_access claim:
        Map<String, Object> realmAccess = idToken.getClaim("realm_access");
        List<GrantedAuthority> authorities = new ArrayList<>();

        if (realmAccess != null) {
            List<String> roles = (List<String>) realmAccess.get("roles");
            if (roles != null) {
                roles.stream()
                    .map(r -> new SimpleGrantedAuthority("ROLE_" + r))
                    .forEach(authorities::add);
            }
        }

        // Add standard OIDC authorities:
        authorities.addAll(oidcUser.getAuthorities());

        return new DefaultOidcUser(
            authorities,
            oidcUser.getIdToken(),
            oidcUser.getUserInfo()
        );
    }
}

// Register in the security config:
.oauth2Login(oauth2 -> oauth2
    .userInfoEndpoint(userInfo -> userInfo
        .oidcUserService(keycloakOidcUserService())
    )
)

Accessing the OAuth2 User in Controllers

After OAuth2 login, the authenticated principal is an OAuth2User or OidcUser. Inject it with @AuthenticationPrincipal in controller methods to access provider attributes and the local user record.
Java
@RestController
@RequestMapping("/api/me")
public class MeController {

    // ── OAuth2User — for non-OIDC providers (GitHub, Facebook): ──────
    @GetMapping("/oauth2")
    public Map<String, Object> getOAuth2User(
            @AuthenticationPrincipal OAuth2User principal) {
        return Map.of(
            "name",       principal.getName(),
            "attributes", principal.getAttributes(),
            "authorities", principal.getAuthorities()
        );
    }

    // ── OidcUser — for OpenID Connect providers (Google, Keycloak): ──
    @GetMapping("/oidc")
    public Map<String, Object> getOidcUser(
            @AuthenticationPrincipal OidcUser principal) {
        return Map.of(
            "subject",   principal.getSubject(),
            "email",     principal.getEmail(),
            "name",      principal.getFullName(),
            "picture",   principal.getPicture(),
            "locale",    principal.getLocale(),
            "claims",    principal.getClaims(),
            "idToken",   principal.getIdToken().getTokenValue()
        );
    }

    // ── CustomOAuth2User — access local user record: ──────────────────
    @GetMapping
    public UserProfileResponse getProfile(
            @AuthenticationPrincipal CustomOAuth2User principal) {
        return UserProfileResponse.from(principal.getLocalUser());
    }

    // ── Generic — works for both OAuth2 and form login: ───────────────
    @GetMapping("/any")
    public String getCurrentUser(Authentication authentication) {
        return authentication.getName();
    }
}

// ── Thymeleaf — display OAuth2 user details: ──────────────────────────
// <div sec:authorize="isAuthenticated()">
//     <p>Welcome, <span sec:authentication="name">User</span></p>
//     <img th:src="${#authentication.principal.attributes['picture']}"
//          th:if="${#authentication.principal.attributes['picture'] != null}" />
// </div>