Spring BootGoogle Login
Spring Boot

Google Login

Google login uses OpenID Connect (OIDC) on top of OAuth2. Spring Boot auto-configures the Google provider — only a client-id and client-secret are required. The ID token contains the user's email, name, and profile picture, verified by Google's public keys. This entry covers Google Cloud Console setup, configuration, OIDC user info extraction, and profile picture handling.

Google Cloud Console Setup

Create OAuth2 credentials in Google Cloud Console before configuring the application. The redirect URI must exactly match what Spring Security registers — any mismatch causes a redirect_uri_mismatch error from Google.
yaml
# ── Google Cloud Console steps ────────────────────────────────────────
# 1. Go to console.cloud.google.com
# 2. Create a project (or select existing)
# 3. APIs & Services → OAuth consent screen
#    - Choose External (for public) or Internal (G Suite only)
#    - Add scopes: openid, email, profile
#    - Add test users if app is in Testing mode
#
# 4. APIs & Services → Credentials → Create Credentials
#    → OAuth client ID → Web application
#
# 5. Add Authorized redirect URIs:
#    Development: http://localhost:8080/login/oauth2/code/google
#    Production:  https://myapp.com/login/oauth2/code/google
#
# 6. Copy the Client ID and Client Secret

# ── application.yml ────────────────────────────────────────────────────
spring:
  security:
    oauth2:
      client:
        registration:
          google:
            client-id:     ${GOOGLE_CLIENT_ID}
            client-secret: ${GOOGLE_CLIENT_SECRET}
            scope:
              - openid     # required for OIDC and ID token
              - profile    # name, picture, locale
              - email      # email address
            redirect-uri: >
              {baseUrl}/login/oauth2/code/{registrationId}
            client-name: Google
            # Authorization code flow — default, do not change
            authorization-grant-type: authorization_code

# ── application-prod.yml ──────────────────────────────────────────────
spring:
  security:
    oauth2:
      client:
        registration:
          google:
            client-id:     ${GOOGLE_CLIENT_ID}
            client-secret: ${GOOGLE_CLIENT_SECRET}
            scope:
              - openid
              - profile
              - email
            # Force account selection on every login
            # (prevents auto-login with the previously used account)
            extra-params:
              prompt: select_account

OidcUserService for Google

Google provides an OIDC ID token in addition to the OAuth2 access token. Spring Security parses it automatically and populates an OidcUser principal. Use OidcUserService instead of DefaultOAuth2UserService to access OIDC-specific claims such as email_verified, sub, and picture.
Java
// ── Google OIDC user service ───────────────────────────────────────────
@Service
@RequiredArgsConstructor
@Slf4j
public class GoogleOidcUserService extends OidcUserService {

    private final AppUserRepository      userRepo;
    private final SocialAccountRepository socialRepo;

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

        OidcUser oidcUser = super.loadUser(userRequest);
        return processGoogleUser(oidcUser);
    }

    private OidcUser processGoogleUser(OidcUser oidcUser) {
        // ── Extract Google OIDC claims ─────────────────────────────────
        String googleId      = oidcUser.getSubject();      // "sub" claim
        String email         = oidcUser.getEmail();
        String name          = oidcUser.getFullName();
        String pictureUrl    = oidcUser.getPicture();
        boolean emailVerified = oidcUser.getEmailVerified() != null
            && oidcUser.getEmailVerified();
        String locale        = oidcUser.getLocale();

        if (!emailVerified) {
            throw new OAuth2AuthenticationException(
                new OAuth2Error("email_not_verified"),
                "Google account email is not verified");
        }

        // ── Find or create application user ───────────────────────────
        AppUser appUser = socialRepo
            .findByProviderAndProviderId("google", googleId)
            .map(SocialAccount::getUser)
            .orElseGet(() -> registerOrLink(
                googleId, email, name, pictureUrl));

        // ── Update profile picture on each login ──────────────────────
        if (pictureUrl != null &&
                !pictureUrl.equals(appUser.getPictureUrl())) {
            appUser.setPictureUrl(pictureUrl);
            userRepo.save(appUser);
        }

        log.info("Google login: {} ({})", email, googleId);
        return appUser;   // AppUser implements OidcUser
    }

    private AppUser registerOrLink(String googleId, String email,
                                    String name, String pictureUrl) {
        AppUser user = userRepo.findByEmail(email)
            .orElseGet(() -> {
                AppUser newUser = new AppUser();
                newUser.setEmail(email);
                newUser.setName(name);
                newUser.setPictureUrl(pictureUrl);
                newUser.setPassword("");
                newUser.setEnabled(true);
                newUser.setEmailVerified(true);
                newUser.setRoles(Set.of("USER"));
                return userRepo.save(newUser);
            });

        SocialAccount social = new SocialAccount();
        social.setProvider("google");
        social.setProviderId(googleId);
        social.setUser(user);
        social.setLinkedAt(LocalDateTime.now());
        socialRepo.save(social);

        return user;
    }
}

// ── Register OidcUserService in SecurityFilterChain ───────────────────
@Bean
public SecurityFilterChain filterChain(HttpSecurity http,
        GoogleOidcUserService oidcUserService) throws Exception {
    return http
        .oauth2Login(oauth2 -> oauth2
            .userInfoEndpoint(ep -> ep
                .oidcUserService(oidcUserService)))  // OIDC-aware
        .build();
}

Extracting Google Claims

The Google ID token contains a rich set of standard OIDC claims. Access them through the OidcUser interface or directly from the ID token. For REST APIs, include relevant user attributes in the JWT payload so downstream services have user context without an extra lookup.
Java
// ── AppUser implementing OidcUser ─────────────────────────────────────
@Entity
@Table(name = "users")
@Getter @Setter @NoArgsConstructor
public class AppUser implements OidcUser, UserDetails {

    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false, unique = true) private String email;
    @Column(nullable = false)                private String name;
    @Column(nullable = false)                private String password;
    @Column(name = "picture_url")            private String pictureUrl;
    @Column(name = "locale", length = 10)    private String locale;
    @Column(name = "email_verified")         private boolean emailVerified;
    @Column(nullable = false)                private boolean enabled = true;

    @ElementCollection(fetch = FetchType.EAGER)
    @CollectionTable(name = "user_roles",
                     joinColumns = @JoinColumn(name = "user_id"))
    @Column(name = "role")
    private Set<String> roles = new HashSet<>();

    // ── OidcUser — transient fields populated after load ──────────────
    @Transient private OidcIdToken      idToken;
    @Transient private OidcUserInfo     userInfo;
    @Transient private Map<String, Object> attributes = new HashMap<>();

    @Override public Map<String, Object> getClaims()    { return attributes; }
    @Override public OidcUserInfo        getUserInfo()  { return userInfo;   }
    @Override public OidcIdToken         getIdToken()   { return idToken;    }
    @Override public Map<String, Object> getAttributes(){ return attributes; }
    @Override public String              getName()      { return email;      }

    // ── UserDetails ───────────────────────────────────────────────────
    @Override public String  getUsername()              { return email; }
    @Override public boolean isAccountNonExpired()      { return true;  }
    @Override public boolean isAccountNonLocked()       { return true;  }
    @Override public boolean isCredentialsNonExpired()  { return true;  }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return roles.stream()
            .map(r -> new SimpleGrantedAuthority(
                r.startsWith("ROLE_") ? r : "ROLE_" + r))
            .toList();
    }
}

// ── Google claims reference ────────────────────────────────────────────
// oidcUser.getSubject()         → "118464973519..." (stable Google ID)
// oidcUser.getEmail()           → "alice@gmail.com"
// oidcUser.getEmailVerified()   → true
// oidcUser.getFullName()        → "Alice Smith"
// oidcUser.getGivenName()       → "Alice"
// oidcUser.getFamilyName()      → "Smith"
// oidcUser.getPicture()         → "https://lh3.googleusercontent.com/..."
// oidcUser.getLocale()          → "en"
// oidcUser.getIssuer()          → "https://accounts.google.com"
// oidcUser.getIssuedAt()        → Instant
// oidcUser.getExpiresAt()       → Instant

// ── Include Google claims in JWT ──────────────────────────────────────
public String generateAccessToken(AppUser user) {
    return Jwts.builder()
        .subject(user.getEmail())
        .claim("userId",    user.getId())
        .claim("name",      user.getName())
        .claim("picture",   user.getPictureUrl())
        .claim("roles",     user.getRoles())
        .issuedAt(new Date())
        .expiration(new Date(System.currentTimeMillis() + 900_000))
        .signWith(secretKey)
        .compact();
}

Google One Tap and Token Verification

Google One Tap allows sign-in without the full redirect flow — the user consents in a popup and the frontend receives a credential (ID token) directly. The backend verifies it using Google's token verification library without Spring Security's OAuth2 login filter.
XML
<!-- pom.xml — Google API client for server-side token verification -->
<dependency>
    <groupId>com.google.api-client</groupId>
    <artifactId>google-api-client</artifactId>
    <version>2.4.0</version>
</dependency>

// ── Verify Google credential from One Tap ─────────────────────────────
@Service
@Slf4j
public class GoogleTokenVerifier {

    @Value("${GOOGLE_CLIENT_ID}")
    private String clientId;

    public GoogleIdToken.Payload verify(String credential) {
        try {
            GoogleIdTokenVerifier verifier =
                new GoogleIdTokenVerifier.Builder(
                    new NetHttpTransport(),
                    GsonFactory.getDefaultInstance())
                .setAudience(List.of(clientId))
                .build();

            GoogleIdToken idToken = verifier.verify(credential);
            if (idToken == null) {
                throw new InvalidTokenException(
                    "Invalid Google credential");
            }
            return idToken.getPayload();
        } catch (Exception ex) {
            throw new InvalidTokenException(
                "Google token verification failed: " + ex.getMessage());
        }
    }
}

// ── Controller — accept credential from Google One Tap ────────────────
@RestController
@RequestMapping("/api/v1/auth/google")
@RequiredArgsConstructor
public class GoogleAuthController {

    private final GoogleTokenVerifier tokenVerifier;
    private final CustomOAuth2UserService userService;
    private final JwtService jwtService;

    public record OneTapRequest(@NotBlank String credential) {}

    @PostMapping("/one-tap")
    public ResponseEntity<AuthResponse> oneTap(
            @RequestBody @Valid OneTapRequest request) {

        GoogleIdToken.Payload payload =
            tokenVerifier.verify(request.credential());

        String googleId = payload.getSubject();
        String email    = payload.getEmail();
        String name     = (String) payload.get("name");
        String picture  = (String) payload.get("picture");
        boolean verified = payload.getEmailVerified();

        if (!verified) {
            return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
                .build();
        }

        AppUser user = userService.findOrCreateGoogleUser(
            googleId, email, name, picture);

        String accessToken  = jwtService.generateAccessToken(user);
        String refreshToken = jwtService.generateRefreshToken(user);

        return ResponseEntity.ok(new AuthResponse(
            accessToken, refreshToken, "Bearer", 900L,
            user.getId(), user.getEmail(),
            user.getRoles().stream().toList()));
    }
}