Wiring OAuth2 login, SSO with LinkedIn and Google, custom JWT issuance after OAuth handshake, and the session vs stateless trade-off you need to make early.
OAuth2 in Spring Boot is well-documented but the tutorials stop at 'user can log in with Google'. What they don't cover is issuing your own JWT after the OAuth handshake, custom user registration on first login, and making the whole thing stateless.
1@Component
2@RequiredArgsConstructor
3public class OAuth2SuccessHandler extends SimpleUrlAuthenticationSuccessHandler {
4
5 private final UserService userService;
6 private final JwtService jwtService;
7
8 @Override
9 public void onAuthenticationSuccess(HttpServletRequest req, HttpServletResponse res,
10 Authentication auth) throws IOException {
11 OAuth2User oauthUser = (OAuth2User) auth.getPrincipal();
12 String email = oauthUser.getAttribute("email");
13 String name = oauthUser.getAttribute("name");
14
15 // Create user on first login, fetch existing on subsequent logins
16 User user = userService.findOrCreateByEmail(email, name);
17
18 String token = jwtService.generateToken(user);
19
20 // Redirect to frontend with token in query param (or set cookie)
21 String redirectUrl = UriComponentsBuilder
22 .fromUriString("https://yourapp.com/auth/callback")
23 .queryParam("token", token)
24 .build().toUriString();
25
26 getRedirectStrategy().sendRedirect(req, res, redirectUrl);
27 }
28}Don't pass JWTs in URL query params in production — they end up in server logs and browser history. Use HttpOnly cookies or have the frontend exchange a short-lived code for the token via a POST request.
1@Service
2public class CustomOAuth2UserService extends DefaultOAuth2UserService {
3
4 private final UserRepository userRepo;
5
6 @Override
7 public OAuth2User loadUser(OAuth2UserRequest request) throws OAuth2AuthenticationException {
8 OAuth2User oauthUser = super.loadUser(request);
9
10 String provider = request.getClientRegistration().getRegistrationId(); // "google", "linkedin"
11 String email = extractEmail(oauthUser, provider);
12 String name = oauthUser.getAttribute("name");
13
14 User user = userRepo.findByEmail(email)
15 .map(u -> { u.setName(name); return userRepo.save(u); })
16 .orElseGet(() -> userRepo.save(
17 User.builder().email(email).name(name)
18 .provider(provider).role(Role.USER).build()
19 ));
20
21 return new CustomUserDetails(user, oauthUser.getAttributes());
22 }
23
24 private String extractEmail(OAuth2User user, String provider) {
25 if ("linkedin".equals(provider)) {
26 // LinkedIn uses emailAddress, not email
27 return user.getAttribute("emailAddress");
28 }
29 return user.getAttribute("email");
30 }
31}1spring:
2 security:
3 oauth2:
4 client:
5 registration:
6 google:
7 client-id: ${GOOGLE_CLIENT_ID}
8 client-secret: ${GOOGLE_CLIENT_SECRET}
9 scope: openid, email, profile
10 linkedin:
11 client-id: ${LINKEDIN_CLIENT_ID}
12 client-secret: ${LINKEDIN_CLIENT_SECRET}
13 scope: r_liteprofile, r_emailaddress
14 authorization-grant-type: authorization_code
15 redirect-uri: "{baseUrl}/login/oauth2/code/{registrationId}"
16 provider:
17 linkedin:
18 authorization-uri: https://www.linkedin.com/oauth/v2/authorization
19 token-uri: https://www.linkedin.com/oauth/v2/accessToken
20 user-info-uri: https://api.linkedin.com/v2/meMore in Backend Engineering