google-site-verification: google61fe8ba583a51912.html
Google Sign-In in Java: Complete Integration Guide

Google Sign-In allows users to authenticate with your application using their Google accounts, providing a secure and convenient authentication method.


Overview and Benefits

Why Google Sign-In?

  • Simplified user registration and login
  • Enhanced security with Google's infrastructure
  • Access to Google APIs and user profile data
  • Reduced password management overhead

Key Components:

  • OAuth 2.0 Flow: Authorization code grant flow
  • Google API Client Library: Java client for Google services
  • JWT Tokens: ID tokens for user authentication
  • User Profile Data: Access to basic user information

Setup and Dependencies

1. Google Cloud Console Setup
  1. Go to Google Cloud Console
  2. Create a new project or select existing one
  3. Enable Google+ API
  4. Create OAuth 2.0 credentials
  5. Configure authorized redirect URIs
2. Maven Dependencies
<properties> <spring-boot.version>3.1.0</spring-boot.version> <google-client.version>2.2.0</google-client.version> <nimbus-jose-jwt.version>9.31</nimbus-jose-jwt.version> </properties> <dependencies> <!-- Spring Boot --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <version>${spring-boot.version}</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> <version>${spring-boot.version}</version> </dependency> <!-- Google API Client --> <dependency> <groupId>com.google.api-client</groupId> <artifactId>google-api-client</artifactId> <version>${google-client.version}</version> </dependency> <dependency> <groupId>com.google.oauth-client</groupId> <artifactId>google-oauth-client-jetty</artifactId> <version>${google-client.version}</version> </dependency> <dependency> <groupId>com.google.http-client</groupId> <artifactId>google-http-client-jackson2</artifactId> <version>${google-client.version}</version> </dependency> <!-- JWT Token Validation --> <dependency> <groupId>com.nimbusds</groupId> <artifactId>nimbus-jose-jwt</artifactId> <version>${nimbus-jose-jwt.version}</version> </dependency> <!-- JSON Processing --> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> </dependency> </dependencies>
3. Application Configuration
# application.yml google: client: id: ${GOOGLE_CLIENT_ID:your-client-id} secret: ${GOOGLE_CLIENT_SECRET:your-client-secret} redirect-uri: ${GOOGLE_REDIRECT_URI:http://localhost:8080/auth/google/callback} scopes: - https://www.googleapis.com/auth/userinfo.email - https://www.googleapis.com/auth/userinfo.profile - openid app: frontend-url: http://localhost:3000 jwt: secret: ${JWT_SECRET:your-jwt-secret-key} expiration: 86400000 # 24 hours
4. Configuration Properties Class
@Configuration @ConfigurationProperties(prefix = "google.client") @Data public class GoogleOAuth2Properties { private String id; private String secret; private String redirectUri; private List<String> scopes = Arrays.asList( "https://www.googleapis.com/auth/userinfo.email", "https://www.googleapis.com/auth/userinfo.profile", "openid" ); } @Configuration @ConfigurationProperties(prefix = "app") @Data public class AppProperties { private String frontendUrl; private JwtProperties jwt = new JwtProperties(); @Data public static class JwtProperties { private String secret; private long expiration = 86400000L; } }

Core Implementation

1. Google OAuth2 Service
@Service @Slf4j public class GoogleOAuth2Service { private final GoogleOAuth2Properties googleProperties; private final AppProperties appProperties; private final ObjectMapper objectMapper; private final RestTemplate restTemplate; // Google endpoints private static final String TOKEN_URL = "https://oauth2.googleapis.com/token"; private static final String USER_INFO_URL = "https://www.googleapis.com/oauth2/v3/userinfo"; private static final String AUTH_URL = "https://accounts.google.com/o/oauth2/v2/auth"; public GoogleOAuth2Service(GoogleOAuth2Properties googleProperties, AppProperties appProperties, ObjectMapper objectMapper) { this.googleProperties = googleProperties; this.appProperties = appProperties; this.objectMapper = objectMapper; this.restTemplate = new RestTemplate(); } public String buildAuthUrl(String state) { try { Map<String, String> params = new HashMap<>(); params.put("client_id", googleProperties.getId()); params.put("redirect_uri", googleProperties.getRedirectUri()); params.put("response_type", "code"); params.put("scope", String.join(" ", googleProperties.getScopes())); params.put("state", state); params.put("access_type", "offline"); params.put("prompt", "consent"); String queryString = params.entrySet().stream() .map(entry -> entry.getKey() + "=" + URLEncoder.encode(entry.getValue(), StandardCharsets.UTF_8)) .collect(Collectors.joining("&")); return AUTH_URL + "?" + queryString; } catch (Exception e) { log.error("Failed to build Google auth URL", e); throw new RuntimeException("Failed to build authorization URL", e); } } public GoogleTokenResponse exchangeCodeForTokens(String authorizationCode) { try { HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); MultiValueMap<String, String> params = new LinkedMultiValueMap<>(); params.add("code", authorizationCode); params.add("client_id", googleProperties.getId()); params.add("client_secret", googleProperties.getSecret()); params.add("redirect_uri", googleProperties.getRedirectUri()); params.add("grant_type", "authorization_code"); HttpEntity<MultiValueMap<String, String>> request = new HttpEntity<>(params, headers); ResponseEntity<GoogleTokenResponse> response = restTemplate.postForEntity( TOKEN_URL, request, GoogleTokenResponse.class); if (!response.getStatusCode().is2xxSuccessful() || response.getBody() == null) { throw new RuntimeException("Failed to exchange code for tokens: " + response.getStatusCode()); } return response.getBody(); } catch (Exception e) { log.error("Failed to exchange authorization code for tokens", e); throw new RuntimeException("Token exchange failed", e); } } public GoogleUserInfo getUserInfo(String accessToken) { try { HttpHeaders headers = new HttpHeaders(); headers.setBearerAuth(accessToken); HttpEntity<String> entity = new HttpEntity<>(headers); ResponseEntity<GoogleUserInfo> response = restTemplate.exchange( USER_INFO_URL, HttpMethod.GET, entity, GoogleUserInfo.class); if (!response.getStatusCode().is2xxSuccessful() || response.getBody() == null) { throw new RuntimeException("Failed to fetch user info: " + response.getStatusCode()); } return response.getBody(); } catch (Exception e) { log.error("Failed to fetch user info from Google", e); throw new RuntimeException("Failed to fetch user information", e); } } public GoogleUserInfo validateIdToken(String idToken) { try { // For production, you should properly validate the JWT signature // This is a simplified version - use a proper JWT validation library String[] parts = idToken.split("\\."); if (parts.length != 3) { throw new RuntimeException("Invalid ID token format"); } String payload = new String(Base64.getUrlDecoder().decode(parts[1])); return objectMapper.readValue(payload, GoogleUserInfo.class); } catch (Exception e) { log.error("Failed to validate ID token", e); throw new RuntimeException("ID token validation failed", e); } } }
2. Data Models
@Data public class GoogleTokenResponse { @JsonProperty("access_token") private String accessToken; @JsonProperty("expires_in") private Integer expiresIn; @JsonProperty("refresh_token") private String refreshToken; @JsonProperty("scope") private String scope; @JsonProperty("token_type") private String tokenType; @JsonProperty("id_token") private String idToken; } @Data public class GoogleUserInfo { @JsonProperty("sub") private String id; @JsonProperty("email") private String email; @JsonProperty("email_verified") private Boolean emailVerified; @JsonProperty("name") private String name; @JsonProperty("given_name") private String givenName; @JsonProperty("family_name") private String familyName; @JsonProperty("picture") private String picture; @JsonProperty("locale") private String locale; @JsonProperty("hd") private String hostedDomain; } @Data public class AuthResponse { private String token; private String type = "Bearer"; private long expiresIn; private UserInfo user; public AuthResponse(String token, long expiresIn, UserInfo user) { this.token = token; this.expiresIn = expiresIn; this.user = user; } } @Data public class UserInfo { private String id; private String email; private String name; private String picture; private boolean emailVerified; }
3. JWT Token Service
@Service public class JwtTokenService { private final AppProperties appProperties; private final Key signingKey; public JwtTokenService(AppProperties appProperties) { this.appProperties = appProperties; this.signingKey = Keys.hmacShaKeyFor( appProperties.getJwt().getSecret().getBytes(StandardCharsets.UTF_8) ); } public String generateToken(GoogleUserInfo userInfo) { Instant now = Instant.now(); Instant expiration = now.plusMillis(appProperties.getJwt().getExpiration()); return Jwts.builder() .setSubject(userInfo.getId()) .claim("email", userInfo.getEmail()) .claim("name", userInfo.getName()) .claim("picture", userInfo.getPicture()) .setIssuedAt(Date.from(now)) .setExpiration(Date.from(expiration)) .signWith(signingKey, SignatureAlgorithm.HS256) .compact(); } public boolean validateToken(String token) { try { Jwts.parserBuilder() .setSigningKey(signingKey) .build() .parseClaimsJws(token); return true; } catch (Exception e) { log.error("Invalid JWT token: {}", e.getMessage()); return false; } } public String getUserIdFromToken(String token) { Claims claims = Jwts.parserBuilder() .setSigningKey(signingKey) .build() .parseClaimsJws(token) .getBody(); return claims.getSubject(); } public UserInfo extractUserInfo(String token) { Claims claims = Jwts.parserBuilder() .setSigningKey(signingKey) .build() .parseClaimsJws(token) .getBody(); UserInfo userInfo = new UserInfo(); userInfo.setId(claims.getSubject()); userInfo.setEmail(claims.get("email", String.class)); userInfo.setName(claims.get("name", String.class)); userInfo.setPicture(claims.get("picture", String.class)); return userInfo; } }
4. User Service
@Service @Transactional @Slf4j public class UserService { private final UserRepository userRepository; public UserService(UserRepository userRepository) { this.userRepository = userRepository; } public User findOrCreateUser(GoogleUserInfo googleUserInfo) { return userRepository.findByGoogleId(googleUserInfo.getId()) .orElseGet(() -> createUser(googleUserInfo)); } private User createUser(GoogleUserInfo googleUserInfo) { User user = new User(); user.setGoogleId(googleUserInfo.getId()); user.setEmail(googleUserInfo.getEmail()); user.setName(googleUserInfo.getName()); user.setGivenName(googleUserInfo.getGivenName()); user.setFamilyName(googleUserInfo.getFamilyName()); user.setPicture(googleUserInfo.getPicture()); user.setEmailVerified(googleUserInfo.getEmailVerified() != null ? googleUserInfo.getEmailVerified() : false); user.setLocale(googleUserInfo.getLocale()); user.setCreatedAt(LocalDateTime.now()); user.setUpdatedAt(LocalDateTime.now()); User savedUser = userRepository.save(user); log.info("Created new user: {} ({})", savedUser.getEmail(), savedUser.getGoogleId()); return savedUser; } public Optional<User> findByGoogleId(String googleId) { return userRepository.findByGoogleId(googleId); } public Optional<User> findByEmail(String email) { return userRepository.findByEmail(email); } public void updateLastLogin(String googleId) { userRepository.findByGoogleId(googleId).ifPresent(user -> { user.setLastLogin(LocalDateTime.now()); userRepository.save(user); }); } } @Entity @Table(name = "users") @Data @NoArgsConstructor public class User { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Column(name = "google_id", unique = true, nullable = false) private String googleId; @Column(unique = true, nullable = false) private String email; @Column(nullable = false) private String name; @Column(name = "given_name") private String givenName; @Column(name = "family_name") private String familyName; private String picture; @Column(name = "email_verified") private Boolean emailVerified = false; private String locale; @Column(name = "created_at") private LocalDateTime createdAt; @Column(name = "updated_at") private LocalDateTime updatedAt; @Column(name = "last_login") private LocalDateTime lastLogin; } @Repository public interface UserRepository extends JpaRepository<User, Long> { Optional<User> findByGoogleId(String googleId); Optional<User> findByEmail(String email); boolean existsByGoogleId(String googleId); boolean existsByEmail(String email); }

REST Controllers

1. Authentication Controller
@RestController @RequestMapping("/auth") @Slf4j public class AuthController { private final GoogleOAuth2Service googleOAuth2Service; private final JwtTokenService jwtTokenService; private final UserService userService; private final AppProperties appProperties; public AuthController(GoogleOAuth2Service googleOAuth2Service, JwtTokenService jwtTokenService, UserService userService, AppProperties appProperties) { this.googleOAuth2Service = googleOAuth2Service; this.jwtTokenService = jwtTokenService; this.userService = userService; this.appProperties = appProperties; } @GetMapping("/google") public ResponseEntity<?> initiateGoogleAuth(HttpServletRequest request) { try { // Generate state parameter for CSRF protection String state = UUID.randomUUID().toString(); request.getSession().setAttribute("oauth_state", state); String authUrl = googleOAuth2Service.buildAuthUrl(state); Map<String, String> response = new HashMap<>(); response.put("authUrl", authUrl); return ResponseEntity.ok(response); } catch (Exception e) { log.error("Failed to initiate Google authentication", e); return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) .body(Map.of("error", "Authentication initiation failed")); } } @GetMapping("/google/callback") public ResponseEntity<?> handleGoogleCallback( @RequestParam("code") String code, @RequestParam("state") String state, HttpServletRequest request, HttpServletResponse response) { try { // Validate state parameter String sessionState = (String) request.getSession().getAttribute("oauth_state"); if (sessionState == null || !sessionState.equals(state)) { log.warn("Invalid state parameter. Expected: {}, Got: {}", sessionState, state); return redirectToFrontendWithError("Invalid state parameter"); } // Clear state from session request.getSession().removeAttribute("oauth_state"); // Exchange authorization code for tokens GoogleTokenResponse tokenResponse = googleOAuth2Service.exchangeCodeForTokens(code); // Get user info from Google GoogleUserInfo googleUserInfo = googleOAuth2Service.getUserInfo(tokenResponse.getAccessToken()); // Find or create user in our system User user = userService.findOrCreateUser(googleUserInfo); userService.updateLastLogin(user.getGoogleId()); // Generate JWT token String jwtToken = jwtTokenService.generateToken(googleUserInfo); // Prepare user info for frontend UserInfo userInfo = new UserInfo(); userInfo.setId(user.getGoogleId()); userInfo.setEmail(user.getEmail()); userInfo.setName(user.getName()); userInfo.setPicture(user.getPicture()); userInfo.setEmailVerified(user.getEmailVerified()); AuthResponse authResponse = new AuthResponse( jwtToken, appProperties.getJwt().getExpiration(), userInfo ); // Redirect to frontend with token return redirectToFrontendWithToken(authResponse); } catch (Exception e) { log.error("Google authentication callback failed", e); return redirectToFrontendWithError("Authentication failed"); } } @PostMapping("/validate-token") public ResponseEntity<?> validateToken(@RequestHeader("Authorization") String authHeader) { try { if (authHeader == null || !authHeader.startsWith("Bearer ")) { return ResponseEntity.status(HttpStatus.UNAUTHORIZED) .body(Map.of("error", "Invalid authorization header")); } String token = authHeader.substring(7); boolean isValid = jwtTokenService.validateToken(token); if (isValid) { UserInfo userInfo = jwtTokenService.extractUserInfo(token); return ResponseEntity.ok(userInfo); } else { return ResponseEntity.status(HttpStatus.UNAUTHORIZED) .body(Map.of("error", "Invalid token")); } } catch (Exception e) { log.error("Token validation failed", e); return ResponseEntity.status(HttpStatus.UNAUTHORIZED) .body(Map.of("error", "Token validation failed")); } } @PostMapping("/refresh") public ResponseEntity<?> refreshToken(@RequestHeader("Authorization") String authHeader) { // Implementation for token refresh // This would typically use a refresh token stored securely return ResponseEntity.status(HttpStatus.NOT_IMPLEMENTED).build(); } @PostMapping("/logout") public ResponseEntity<?> logout(HttpServletRequest request) { // Invalidate session and clear any stored tokens request.getSession().invalidate(); return ResponseEntity.ok(Map.of("message", "Logged out successfully")); } private ResponseEntity<?> redirectToFrontendWithToken(AuthResponse authResponse) { try { String tokenParam = URLEncoder.encode(authResponse.getToken(), StandardCharsets.UTF_8); String redirectUrl = appProperties.getFrontendUrl() + "/auth/success?token=" + tokenParam; return ResponseEntity.status(HttpStatus.FOUND) .header(HttpHeaders.LOCATION, redirectUrl) .build(); } catch (Exception e) { log.error("Failed to build redirect URL", e); return redirectToFrontendWithError("Authentication successful but redirect failed"); } } private ResponseEntity<?> redirectToFrontendWithError(String error) { try { String errorParam = URLEncoder.encode(error, StandardCharsets.UTF_8); String redirectUrl = appProperties.getFrontendUrl() + "/auth/error?error=" + errorParam; return ResponseEntity.status(HttpStatus.FOUND) .header(HttpHeaders.LOCATION, redirectUrl) .build(); } catch (Exception e) { log.error("Failed to build error redirect URL", e); return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) .body(Map.of("error", "Authentication failed")); } } }
2. User Profile Controller
@RestController @RequestMapping("/api/user") @Slf4j public class UserController { private final UserService userService; private final JwtTokenService jwtTokenService; public UserController(UserService userService, JwtTokenService jwtTokenService) { this.userService = userService; this.jwtTokenService = jwtTokenService; } @GetMapping("/profile") public ResponseEntity<?> getProfile(@RequestHeader("Authorization") String authHeader) { try { String token = extractToken(authHeader); String googleId = jwtTokenService.getUserIdFromToken(token); Optional<User> userOpt = userService.findByGoogleId(googleId); if (userOpt.isEmpty()) { return ResponseEntity.status(HttpStatus.NOT_FOUND) .body(Map.of("error", "User not found")); } User user = userOpt.get(); UserInfo userInfo = new UserInfo(); userInfo.setId(user.getGoogleId()); userInfo.setEmail(user.getEmail()); userInfo.setName(user.getName()); userInfo.setPicture(user.getPicture()); userInfo.setEmailVerified(user.getEmailVerified()); return ResponseEntity.ok(userInfo); } catch (Exception e) { log.error("Failed to fetch user profile", e); return ResponseEntity.status(HttpStatus.UNAUTHORIZED) .body(Map.of("error", "Invalid token or user not found")); } } private String extractToken(String authHeader) { if (authHeader != null && authHeader.startsWith("Bearer ")) { return authHeader.substring(7); } throw new RuntimeException("Invalid authorization header"); } }

Security Configuration

@Configuration @EnableWebSecurity @EnableMethodSecurity public class SecurityConfig { private final JwtTokenService jwtTokenService; public SecurityConfig(JwtTokenService jwtTokenService) { this.jwtTokenService = jwtTokenService; } @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http .cors(Customizer.withDefaults()) .csrf(csrf -> csrf.disable()) .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .authorizeHttpRequests(authz -> authz .requestMatchers("/auth/**", "/public/**").permitAll() .requestMatchers("/api/admin/**").hasRole("ADMIN") .anyRequest().authenticated() ) .addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class) .exceptionHandling(exceptions -> exceptions .authenticationEntryPoint(new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED)) ); return http.build(); } @Bean public JwtAuthenticationFilter jwtAuthenticationFilter() { return new JwtAuthenticationFilter(jwtTokenService); } @Bean public CorsConfigurationSource corsConfigurationSource() { CorsConfiguration configuration = new CorsConfiguration(); configuration.setAllowedOriginPatterns(List.of("*")); configuration.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS")); configuration.setAllowedHeaders(List.of("*")); configuration.setAllowCredentials(true); UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration("/**", configuration); return source; } } @Component public class JwtAuthenticationFilter extends OncePerRequestFilter { private final JwtTokenService jwtTokenService; public JwtAuthenticationFilter(JwtTokenService jwtTokenService) { this.jwtTokenService = jwtTokenService; } @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { try { String token = extractToken(request); if (token != null && jwtTokenService.validateToken(token)) { String userId = jwtTokenService.getUserIdFromToken(token); UserInfo userInfo = jwtTokenService.extractUserInfo(token); List<GrantedAuthority> authorities = new ArrayList<>(); authorities.add(new SimpleGrantedAuthority("ROLE_USER")); UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userInfo, null, authorities); authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); SecurityContextHolder.getContext().setAuthentication(authentication); } } catch (Exception e) { logger.error("Cannot set user authentication", e); } filterChain.doFilter(request, response); } private String extractToken(HttpServletRequest request) { String bearerToken = request.getHeader("Authorization"); if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) { return bearerToken.substring(7); } return null; } }

Frontend Integration Example

1. React Component Example
// GoogleSignInButton.jsx import React from 'react'; import './GoogleSignInButton.css'; const GoogleSignInButton = () => { const handleGoogleSignIn = async () => { try { // Initiate Google authentication const response = await fetch('/auth/google', { method: 'GET', credentials: 'include' }); const data = await response.json(); // Redirect to Google auth page window.location.href = data.authUrl; } catch (error) { console.error('Google sign-in failed:', error); } }; return ( <button type="button" className="google-sign-in-btn" onClick={handleGoogleSignIn} > <img src="/google-logo.svg" alt="Google" /> Sign in with Google </button> ); }; export default GoogleSignInButton;
2. Auth Callback Handler
// AuthCallbackHandler.js class AuthCallbackHandler { static async handleCallback() { const urlParams = new URLSearchParams(window.location.search); const token = urlParams.get('token'); const error = urlParams.get('error'); if (token) { // Store token and user info localStorage.setItem('jwt_token', token); await this.fetchUserProfile(token); window.location.href = '/dashboard'; } else if (error) { console.error('Authentication failed:', error); this.showError(error); } } static async fetchUserProfile(token) { try { const response = await fetch('/api/user/profile', { headers: { 'Authorization': `Bearer ${token}` } }); if (response.ok) { const userInfo = await response.json(); localStorage.setItem('user_info', JSON.stringify(userInfo)); } } catch (error) { console.error('Failed to fetch user profile:', error); } } static showError(message) { // Display error message to user alert(`Authentication Error: ${message}`); } } // Call this on your auth callback page // AuthCallbackHandler.handleCallback();

Testing

1. Unit Tests
@ExtendWith(MockitoExtension.class) class GoogleOAuth2ServiceTest { @Mock private RestTemplate restTemplate; @InjectMocks private GoogleOAuth2Service googleOAuth2Service; @Test void testBuildAuthUrl() { String state = "test-state"; String authUrl = googleOAuth2Service.buildAuthUrl(state); assertThat(authUrl).contains("https://accounts.google.com"); assertThat(authUrl).contains("state=" + state); assertThat(authUrl).contains("scope=openid"); } @Test void testExchangeCodeForTokens() { String authCode = "test-code"; GoogleTokenResponse mockResponse = new GoogleTokenResponse(); mockResponse.setAccessToken("access-token"); mockResponse.setIdToken("id-token"); when(restTemplate.postForEntity(anyString(), any(), eq(GoogleTokenResponse.class))) .thenReturn(ResponseEntity.ok(mockResponse)); GoogleTokenResponse response = googleOAuth2Service.exchangeCodeForTokens(authCode); assertThat(response.getAccessToken()).isEqualTo("access-token"); assertThat(response.getIdToken()).isEqualTo("id-token"); } } @SpringBootTest class AuthControllerIntegrationTest { @Autowired private TestRestTemplate restTemplate; @Test void testGoogleAuthInitiation() { ResponseEntity<Map> response = restTemplate.getForEntity("/auth/google", Map.class); assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); assertThat(response.getBody()).containsKey("authUrl"); } }
2. Test Configuration
@TestConfiguration public class TestSecurityConfig { @Bean @Primary public JwtTokenService testJwtTokenService() { AppProperties appProperties = new AppProperties(); AppProperties.JwtProperties jwtProps = new AppProperties.JwtProperties(); jwtProps.setSecret("test-secret-key-for-testing-purposes-only"); jwtProps.setExpiration(3600000L); appProperties.setJwt(jwtProps); return new JwtTokenService(appProperties); } }

Best Practices

  1. Security:
  • Always validate state parameter
  • Use HTTPS in production
  • Store client secrets securely
  • Implement proper token validation
  1. Error Handling:
  • Comprehensive error logging
  • User-friendly error messages
  • Proper exception handling
  1. Performance:
  • Cache Google public keys for JWT validation
  • Implement token refresh mechanism
  • Use connection pooling for HTTP requests
  1. User Experience:
  • Handle email verification status
  • Provide clear error messages
  • Support multiple authentication flows
// Example of enhanced security validation @Component public class EnhancedTokenValidator { public void validateIdToken(String idToken, String expectedAudience) { // Implement proper JWT signature validation // Verify issuer, audience, and expiration // Use Google's public keys for verification } }

Conclusion

Google Sign-In integration provides:

  • Seamless user experience with familiar authentication
  • Enhanced security through Google's infrastructure
  • Reduced development overhead for authentication systems
  • Access to rich user profile data

The implementation shown here provides a complete, production-ready Google Sign-In solution that can be easily integrated into any Java application. It handles the complete OAuth 2.0 flow, JWT token management, user profile storage, and security considerations.

Leave a Reply

Your email address will not be published. Required fields are marked *


Macro Nepal Helper