OAuth2 Login: Sign In with Google, GitHub, and Custom Providers
What OAuth2 Login Does
OAuth2LoginConfigurer implements the Authorization Code flow for user authentication. When a user clicks “Sign in with Google”:
- Spring redirects to Google’s
/authorizeendpoint - User authenticates on Google and grants consent
- Google redirects back to your app with a
code - Spring exchanges the
codefor tokens (back channel) - Spring fetches the user profile from
/userinfo - Spring creates an
OAuth2Userand stores it in theSecurityContext
Dependencies
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency>
Configuring Google and GitHub
# application.yml
spring:
security:
oauth2:
client:
registration:
google:
client-id: ${GOOGLE_CLIENT_ID}
client-secret: ${GOOGLE_CLIENT_SECRET}
scope: openid, profile, email
redirect-uri: "{baseUrl}/login/oauth2/code/{registrationId}"
github:
client-id: ${GITHUB_CLIENT_ID}
client-secret: ${GITHUB_CLIENT_SECRET}
scope: read:user, user:email
okta:
client-id: ${OKTA_CLIENT_ID}
client-secret: ${OKTA_CLIENT_SECRET}
scope: openid, profile, email
provider:
okta:
issuer-uri: https://dev-XXXXX.okta.com/oauth2/default
# Google and GitHub are pre-configured — no provider block needed
Spring Boot has built-in provider metadata for Google, GitHub, Facebook, Okta, and a few others. For Google/GitHub, you only need registration.
Security Configuration
@Configuration
@EnableWebSecurity
public class OAuth2SecurityConfig {
private final CustomOAuth2UserService oAuth2UserService;
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/", "/public/**", "/error").permitAll()
.anyRequest().authenticated()
)
.oauth2Login(oauth2 -> oauth2
.loginPage("/login") // custom login page
.userInfoEndpoint(userInfo -> userInfo
.userService(oAuth2UserService) // custom user service
)
.successHandler(oAuth2AuthSuccessHandler()) // after login
.failureHandler(oAuth2AuthFailureHandler()) // on error
)
.logout(logout -> logout
.logoutSuccessUrl("/")
.deleteCookies("JSESSIONID")
);
return http.build();
}
}
The Login Page
@Controller
public class LoginController {
@GetMapping("/login")
public String loginPage() {
return "login"; // login.html
}
}
<!-- login.html — links to Spring Security's OAuth2 authorization endpoints -->
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head><title>Login</title></head>
<body>
<h1>Sign In</h1>
<!-- Spring Security auto-generates these URLs: /oauth2/authorization/{registrationId} -->
<a href="/oauth2/authorization/google">
<img src="/icons/google.svg" alt="Sign in with Google"/>
Sign in with Google
</a>
<a href="/oauth2/authorization/github">
<img src="/icons/github.svg" alt="Sign in with GitHub"/>
Sign in with GitHub
</a>
<a href="/oauth2/authorization/okta">
Sign in with Okta
</a>
</body>
</html>
/oauth2/authorization/{registrationId} is a Spring Security endpoint that starts the OAuth2 flow for that provider. You don’t implement it.
OAuth2User: What You Get After Login
After the OAuth2 flow completes, Spring creates an OAuth2User:
@GetMapping("/profile")
public String profile(@AuthenticationPrincipal OAuth2User oauth2User, Model model) {
// For Google (OIDC) — attributes from the id_token + userinfo
String name = oauth2User.getAttribute("name");
String email = oauth2User.getAttribute("email");
String picture = oauth2User.getAttribute("picture");
String googleId = oauth2User.getAttribute("sub");
// For GitHub
String login = oauth2User.getAttribute("login"); // GitHub username
String htmlUrl = oauth2User.getAttribute("html_url");
// Authorities — by default a single authority: ROLE_USER (from "SCOPE_openid" etc.)
Collection<? extends GrantedAuthority> authorities = oauth2User.getAuthorities();
model.addAttribute("user", oauth2User);
return "profile";
}
For OIDC providers (Google, Okta), you get OidcUser (extends OAuth2User) with additional methods:
@GetMapping("/profile")
public String profile(@AuthenticationPrincipal OidcUser oidcUser) {
OidcIdToken idToken = oidcUser.getIdToken();
String subject = oidcUser.getSubject(); // unique user ID (never changes)
String email = oidcUser.getEmail();
String name = oidcUser.getFullName();
Instant expiry = idToken.getExpiresAt();
return "profile";
}
Custom OAuth2UserService: Persisting Users to Database
By default, Spring Security creates an in-memory OAuth2User on each login — no persistence. For real applications, you want to:
- Look up whether this OAuth2 user has registered before (by provider + sub ID)
- Create a local user record on first login
- Return a custom
UserDetails/OAuth2Userwith your app’s roles
@Service
@RequiredArgsConstructor
public class CustomOAuth2UserService extends DefaultOAuth2UserService {
private final UserRepository userRepository;
@Override
@Transactional
public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
OAuth2User oauth2User = super.loadUser(userRequest); // fetch from provider
String provider = userRequest.getClientRegistration().getRegistrationId(); // "google", "github"
Map<String, Object> attributes = oauth2User.getAttributes();
// Extract identity — different providers use different attribute names
String providerId = extractProviderId(provider, attributes);
String email = extractEmail(provider, attributes, oauth2User);
String name = extractName(provider, attributes);
String pictureUrl = extractPicture(provider, attributes);
// Find or create the local user
User user = userRepository.findByProviderAndProviderId(provider, providerId)
.orElseGet(() -> createNewUser(provider, providerId, email, name, pictureUrl));
// Update on each login (name/picture may change)
user.setName(name);
user.setPictureUrl(pictureUrl);
if (user.getEmail() == null && email != null) {
user.setEmail(email);
}
userRepository.save(user);
return new AppOAuth2User(user, attributes, oauth2User.getAuthorities());
}
private User createNewUser(String provider, String providerId,
String email, String name, String pictureUrl) {
User user = new User();
user.setProvider(provider);
user.setProviderId(providerId);
user.setEmail(email);
user.setName(name);
user.setPictureUrl(pictureUrl);
user.setRoles(Set.of(Role.USER)); // default role
user.setEmailVerified(true); // trusted from OAuth provider
return userRepository.save(user);
}
private String extractProviderId(String provider, Map<String, Object> attrs) {
return switch (provider) {
case "google", "okta" -> String.valueOf(attrs.get("sub"));
case "github" -> String.valueOf(attrs.get("id"));
case "facebook" -> String.valueOf(attrs.get("id"));
default -> throw new OAuth2AuthenticationException("Unknown provider: " + provider);
};
}
private String extractEmail(String provider, Map<String, Object> attrs, OAuth2User user) {
Object email = attrs.get("email");
return email != null ? email.toString() : null;
}
private String extractName(String provider, Map<String, Object> attrs) {
return switch (provider) {
case "google", "okta" -> String.valueOf(attrs.getOrDefault("name", ""));
case "github" -> String.valueOf(attrs.getOrDefault("name",
attrs.getOrDefault("login", "")));
default -> "";
};
}
private String extractPicture(String provider, Map<String, Object> attrs) {
return switch (provider) {
case "google", "okta" -> String.valueOf(attrs.getOrDefault("picture", ""));
case "github" -> String.valueOf(attrs.getOrDefault("avatar_url", ""));
default -> "";
};
}
}
Custom OAuth2User
public class AppOAuth2User implements OAuth2User, UserDetails {
private final User user;
private final Map<String, Object> attributes;
private final Collection<? extends GrantedAuthority> oauthAuthorities;
@Override
public Map<String, Object> getAttributes() { return attributes; }
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
// Use app roles, not OAuth2 scopes
return user.getRoles().stream()
.map(role -> new SimpleGrantedAuthority("ROLE_" + role.name()))
.toList();
}
@Override public String getName() { return user.getProviderId(); }
@Override public String getUsername() { return user.getEmail(); }
@Override public String getPassword() { return null; } // no password for OAuth2 users
public Long getId() { return user.getId(); }
public String getEmail() { return user.getEmail(); }
public String getDisplayName() { return user.getName(); }
}
Success Handler: Post-Login Redirect
@Component
public class OAuth2AuthSuccessHandler implements AuthenticationSuccessHandler {
private final RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();
@Override
public void onAuthenticationSuccess(
HttpServletRequest request,
HttpServletResponse response,
Authentication authentication
) throws IOException {
AppOAuth2User user = (AppOAuth2User) authentication.getPrincipal();
// Check if user needs to complete profile (e.g., email missing from GitHub)
if (user.getEmail() == null) {
redirectStrategy.sendRedirect(request, response, "/profile/complete");
return;
}
// Check for saved request (user was redirected from a protected page)
HttpSession session = request.getSession(false);
if (session != null) {
SavedRequest savedRequest = (SavedRequest) session.getAttribute(
HttpSessionRequestCache.class.getName() + "_SAVED_REQUEST"
);
if (savedRequest != null) {
redirectStrategy.sendRedirect(request, response, savedRequest.getRedirectUrl());
return;
}
}
redirectStrategy.sendRedirect(request, response, "/dashboard");
}
}
Adding Custom Providers (Keycloak Example)
For providers not in Spring’s built-in list:
spring:
security:
oauth2:
client:
registration:
keycloak:
client-id: my-app
client-secret: ${KEYCLOAK_SECRET}
authorization-grant-type: authorization_code
scope: openid, profile, email
redirect-uri: "{baseUrl}/login/oauth2/code/keycloak"
provider:
keycloak:
issuer-uri: https://keycloak.example.com/realms/myrealm
# Spring fetches endpoints from: issuer-uri/.well-known/openid-configuration
OIDC User Service for ID Token Claims
For OIDC providers (Google, Okta, Keycloak), use OidcUserService instead of DefaultOAuth2UserService:
@Service
@RequiredArgsConstructor
public class CustomOidcUserService extends OidcUserService {
private final UserRepository userRepository;
@Override
@Transactional
public OidcUser loadUser(OidcUserRequest userRequest) throws OAuth2AuthenticationException {
OidcUser oidcUser = super.loadUser(userRequest);
String provider = userRequest.getClientRegistration().getRegistrationId();
String providerId = oidcUser.getSubject(); // 'sub' claim — stable identifier
User user = userRepository.findByProviderAndProviderId(provider, providerId)
.orElseGet(() -> createUser(oidcUser, provider));
return new AppOidcUser(user, oidcUser.getIdToken(), oidcUser.getUserInfo());
}
}
// Register both services
http.oauth2Login(oauth2 -> oauth2
.userInfoEndpoint(userInfo -> userInfo
.userService(customOAuth2UserService) // for non-OIDC (GitHub)
.oidcUserService(customOidcUserService) // for OIDC (Google, Okta)
)
);
GitHub: Fetching Private Email
GitHub does not include the email in the userinfo response if the user’s email is private. You must make a separate API call:
@Service
@RequiredArgsConstructor
public class GitHubEmailService {
private final RestTemplate restTemplate;
public String fetchPrimaryEmail(String accessToken) {
HttpHeaders headers = new HttpHeaders();
headers.setBearerAuth(accessToken);
headers.set("Accept", "application/vnd.github+json");
HttpEntity<Void> entity = new HttpEntity<>(headers);
ResponseEntity<List<Map<String, Object>>> response = restTemplate.exchange(
"https://api.github.com/user/emails",
HttpMethod.GET,
entity,
new ParameterizedTypeReference<>() {}
);
return Optional.ofNullable(response.getBody())
.flatMap(emails -> emails.stream()
.filter(e -> Boolean.TRUE.equals(e.get("primary"))
&& Boolean.TRUE.equals(e.get("verified")))
.map(e -> (String) e.get("email"))
.findFirst())
.orElse(null);
}
}
// In CustomOAuth2UserService.loadUser(), after super.loadUser():
if ("github".equals(provider)) {
String accessToken = userRequest.getAccessToken().getTokenValue();
email = gitHubEmailService.fetchPrimaryEmail(accessToken);
}
Summary
- Add
spring-boot-starter-oauth2-clientand configure provider credentials inapplication.yml. - Spring auto-provides
registrationId-specific URLs:/oauth2/authorization/{id}starts the flow,/login/oauth2/code/{id}is the callback. - Implement
DefaultOAuth2UserService(for non-OIDC) orOidcUserService(for OIDC) to persist OAuth2 users to your database on first login. - Extract the stable identifier from
sub(OIDC) orid(GitHub/Facebook) — not from email (users can change it). - Use a custom
AuthenticationSuccessHandlerfor post-login redirects and profile completion flows.
Next: Article 14 covers the OAuth2 Resource Server — protecting your own REST API with JWT or opaque tokens issued by an authorization server.