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
- Go to Google Cloud Console
- Create a new project or select existing one
- Enable Google+ API
- Create OAuth 2.0 credentials
- 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
- Security:
- Always validate state parameter
- Use HTTPS in production
- Store client secrets securely
- Implement proper token validation
- Error Handling:
- Comprehensive error logging
- User-friendly error messages
- Proper exception handling
- Performance:
- Cache Google public keys for JWT validation
- Implement token refresh mechanism
- Use connection pooling for HTTP requests
- 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.