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 sessionSecurity 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>