Backend EngineeringJanuary 12, 20258 min read

Spring Security: OAuth2 + SSO Integration Deep Dive

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.

JavaSpring BootSpring SecurityOAuth2SSOJWT

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.

The Flow

  • 1User clicks 'Login with LinkedIn'
  • 2Spring redirects to LinkedIn authorization endpoint
  • 3LinkedIn calls your /oauth2/callback with an authorization code
  • 4Spring exchanges code for access token, fetches user profile from LinkedIn
  • 5Your OAuth2SuccessHandler receives the OAuth2User, creates/fetches your own user record
  • 6You issue your own JWT and redirect the client with it
OAuth2SuccessHandler.java
java
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.

Custom UserDetails for OAuth2 Users

CustomOAuth2UserService.java
java
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}

application.yml OAuth2 Config

application.yml
java
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/me

More in Backend Engineering