🔐 Step 4: Production Implementation
🧭 Series Navigation:
- ▶️ Spring Security Hub
- ▶️ Step 1: Architecture Foundation
- ▶️ Step 2: Configuration Fundamentals
- ▶️ Step 3: Implementation Strategy
- ✅ Step 4: Production Implementation (Current)
- ▶️ Step 5: Testing & Validation
🎥 YouTube Video: Coming Soon – Subscribe to JavaiOS Channel
AdventureTube Authentication Flow Implementation 🔐
This post details the complete implementation of Spring Security authentication flow in the AdventureTube auth-service module. We’ll walk through the step-by-step process of how Google ID token authentication is implemented, how custom authentication providers are registered, and how the entire authentication flow works in practice.
🏗️ Authentication Flow Architecture
The AdventureTube authentication system implements a sophisticated flow that combines:
- Google ID Token Validation – Primary authentication mechanism
- Custom Authentication Provider – Handles business logic and validation
- JWT Token Generation – Issues access and refresh tokens
- Microservice Communication – Integrates with member-service for user data
High-Level Flow Diagram
Client Request → AuthController → AuthService → CustomAuthenticationProvider
↓ ↓ ↓ ↓
Google ID Validate Load User Authenticate
Token Request Details & Generate JWT
🔧 Core Implementation Components
1. Custom Authentication Provider
The heart of our authentication system is the CustomAuthenticationProvider
:
@Component
public class CustomAuthenticationProvider implements AuthenticationProvider {
private final CustomUserDetailService customUserDetailService;
private final PasswordEncoder passwordEncoder;
public CustomAuthenticationProvider(
CustomUserDetailService customUserDetailService,
PasswordEncoder passwordEncoder) {
this.customUserDetailService = customUserDetailService;
this.passwordEncoder = passwordEncoder;
}
@Override
public Authentication authenticate(Authentication authentication)
throws AuthenticationException {
String email = authentication.getName();
String googleId = authentication.getCredentials().toString();
log.info("Authenticating user with email: {}", email);
try {
// Load user details from member-service
UserDetails userDetails = customUserDetailService.loadUserByUsername(email);
// Validate Google ID
if (userDetails instanceof CustomUserDetails customUser) {
if (!googleId.equals(customUser.getGoogleId())) {
log.warn("Google ID mismatch for user: {}", email);
throw new BadCredentialsException("Invalid credentials");
}
log.info("Authentication successful for user: {}", email);
return new UsernamePasswordAuthenticationToken(
userDetails, googleId, userDetails.getAuthorities());
}
throw new AuthenticationServiceException("Invalid user details type");
} catch (UsernameNotFoundException e) {
log.warn("User not found: {}", email);
throw new BadCredentialsException("Invalid credentials");
} catch (Exception e) {
log.error("Authentication error for user: {}", email, e);
throw new AuthenticationServiceException("Authentication failed");
}
}
@Override
public boolean supports(Class<?> authentication) {
return authentication.equals(UsernamePasswordAuthenticationToken.class);
}
}
2. Custom User Details Service
This service integrates with the member-service to load user information:
@Service
public class CustomUserDetailService implements UserDetailsService {
private final RestTemplate restTemplate;
@Value("${member.service.url}")
private String memberServiceUrl;
public CustomUserDetailService(RestTemplate restTemplate) {
this.restTemplate = restTemplate;
}
@Override
public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException {
try {
log.debug("Loading user details for email: {}", email);
// Call member-service to get user information
String url = memberServiceUrl + "/api/members/email/" + email;
MemberDTO memberDTO = restTemplate.getForObject(url, MemberDTO.class);
if (memberDTO == null) {
log.warn("User not found in member service: {}", email);
throw new UsernameNotFoundException("User not found: " + email);
}
log.debug("User found: {} with role: {}", email, memberDTO.getRole());
// Convert to UserDetails
return CustomUserDetails.builder()
.username(memberDTO.getEmail())
.password(memberDTO.getPassword())
.googleId(memberDTO.getGoogleId())
.role(memberDTO.getRole())
.enabled(true)
.accountNonExpired(true)
.accountNonLocked(true)
.credentialsNonExpired(true)
.build();
} catch (RestClientException e) {
log.error("Error calling member service for user: {}", email, e);
throw new AuthenticationServiceException("User service unavailable");
}
}
}
3. Security Configuration
The security configuration registers our custom provider and sets up the filter chain:
@Configuration
@EnableWebSecurity
public class AuthServiceConfig {
private final CustomAuthenticationProvider customAuthenticationProvider;
private final JwtAuthFilter jwtAuthFilter;
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http
.csrf(csrf -> csrf.disable())
.authorizeHttpRequests(auth -> auth
.requestMatchers(SecurityConstants.OPEN_ENDPOINTS).permitAll()
.requestMatchers("/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()
)
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
)
.authenticationProvider(customAuthenticationProvider)
.addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class)
.exceptionHandling(ex -> ex
.authenticationEntryPoint(jwtAuthenticationEntryPoint())
)
.build();
}
@Bean
public AuthenticationManager authenticationManager(
AuthenticationConfiguration config) throws Exception {
return config.getAuthenticationManager();
}
@Bean
public JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint() {
return new JwtAuthenticationEntryPoint();
}
}
🚀 Complete Authentication Flow
Step 1: Client Login Request
The authentication process begins when a client sends a login request:
@RestController
@RequestMapping("/auth")
public class AuthController {
private final AuthService authService;
@PostMapping("/token")
public ResponseEntity<AuthResponse> issueToken(@RequestBody AuthRequest request) {
try {
log.info("Token request received for email: {}", request.getEmail());
AuthResponse response = authService.issueToken(request);
log.info("Token issued successfully for user: {}", request.getEmail());
return ResponseEntity.ok(response);
} catch (AuthenticationException e) {
log.warn("Authentication failed for user: {}", request.getEmail());
return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
.body(AuthResponse.error("Authentication failed"));
}
}
}
Step 2: Google ID Token Validation
The auth service validates the Google ID token before proceeding:
@Service
public class AuthService {
private final AuthenticationManager authenticationManager;
private final JwtUtil jwtUtil;
private final GoogleIdTokenVerifier tokenVerifier;
public AuthResponse issueToken(AuthRequest request) {
// 1. Validate Google ID token
GoogleIdToken idToken = validateGoogleToken(request.getGoogleIdToken());
String email = idToken.getPayload().getEmail();
// 2. Verify email matches request
if (!email.equals(request.getEmail())) {
throw new BadCredentialsException("Email mismatch");
}
// 3. Create authentication token
UsernamePasswordAuthenticationToken authToken =
new UsernamePasswordAuthenticationToken(
email,
request.getGoogleId()
);
// 4. Authenticate using custom provider
Authentication authentication = authenticationManager.authenticate(authToken);
// 5. Generate JWT tokens
UserDetails userDetails = (UserDetails) authentication.getPrincipal();
String accessToken = jwtUtil.generateAccessToken(userDetails);
String refreshToken = jwtUtil.generateRefreshToken(userDetails);
// 6. Store refresh token
refreshTokenService.storeRefreshToken(email, refreshToken);
return AuthResponse.builder()
.accessToken(accessToken)
.refreshToken(refreshToken)
.expiresIn(jwtUtil.getAccessTokenExpiration())
.tokenType("Bearer")
.build();
}
private GoogleIdToken validateGoogleToken(String tokenString) {
try {
GoogleIdToken idToken = tokenVerifier.verify(tokenString);
if (idToken == null) {
throw new BadCredentialsException("Invalid Google ID token");
}
return idToken;
} catch (Exception e) {
log.error("Google token validation failed", e);
throw new BadCredentialsException("Invalid Google ID token");
}
}
}
Step 3: JWT Token Generation
The JWT utility generates both access and refresh tokens:
@Component
public class JwtUtil {
@Value("${jwt.secret}")
private String secret;
@Value("${jwt.access.expiration}")
private long accessTokenExpiration;
@Value("${jwt.refresh.expiration}")
private long refreshTokenExpiration;
public String generateAccessToken(UserDetails userDetails) {
Map<String, Object> claims = new HashMap<>();
claims.put("role", userDetails.getAuthorities().iterator().next().getAuthority());
claims.put("type", "access");
return createToken(claims, userDetails.getUsername(), accessTokenExpiration);
}
public String generateRefreshToken(UserDetails userDetails) {
Map<String, Object> claims = new HashMap<>();
claims.put("type", "refresh");
return createToken(claims, userDetails.getUsername(), refreshTokenExpiration);
}
private String createToken(Map<String, Object> claims, String subject, long expiration) {
return Jwts.builder()
.setClaims(claims)
.setSubject(subject)
.setIssuedAt(new Date(System.currentTimeMillis()))
.setExpiration(new Date(System.currentTimeMillis() + expiration))
.signWith(SignatureAlgorithm.HS512, secret)
.compact();
}
}
🔒 JWT Authentication Filter
For subsequent requests, the JWT filter handles authentication:
@Component
public class JwtAuthFilter extends OncePerRequestFilter {
private final JwtUtil jwtUtil;
private final CustomUserDetailService userDetailsService;
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
// Skip filtering for open endpoints
if (isOpenEndpoint(request.getRequestURI())) {
filterChain.doFilter(request, response);
return;
}
String authHeader = request.getHeader("Authorization");
String token = null;
String username = null;
if (authHeader != null && authHeader.startsWith("Bearer ")) {
token = authHeader.substring(7);
try {
username = jwtUtil.extractUsername(token);
} catch (Exception e) {
log.warn("Invalid JWT token: {}", e.getMessage());
}
}
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
if (jwtUtil.validateToken(token, userDetails)) {
UsernamePasswordAuthenticationToken authToken =
new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities());
authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authToken);
log.debug("JWT authentication successful for user: {}", username);
}
}
filterChain.doFilter(request, response);
}
private boolean isOpenEndpoint(String requestURI) {
return Arrays.stream(SecurityConstants.OPEN_ENDPOINTS)
.anyMatch(endpoint -> requestURI.matches(endpoint.replace("**", ".*")));
}
}
📊 Error Handling and Security
Custom Authentication Entry Point
@Component
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request,
HttpServletResponse response,
AuthenticationException authException) throws IOException {
log.warn("Unauthorized access attempt: {}", authException.getMessage());
response.setContentType("application/json");
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
ObjectMapper mapper = new ObjectMapper();
ErrorResponse errorResponse = ErrorResponse.builder()
.error("Unauthorized")
.message("Access token is missing or invalid")
.timestamp(Instant.now())
.path(request.getRequestURI())
.build();
response.getWriter().write(mapper.writeValueAsString(errorResponse));
}
}
Global Exception Handler
@RestControllerAdvice
public class AuthExceptionHandler {
@ExceptionHandler(BadCredentialsException.class)
public ResponseEntity<ErrorResponse> handleBadCredentials(BadCredentialsException e) {
log.warn("Bad credentials: {}", e.getMessage());
ErrorResponse error = ErrorResponse.builder()
.error("Authentication Failed")
.message("Invalid email or Google ID")
.timestamp(Instant.now())
.build();
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(error);
}
@ExceptionHandler(AuthenticationServiceException.class)
public ResponseEntity<ErrorResponse> handleAuthServiceException(
AuthenticationServiceException e) {
log.error("Authentication service error: {}", e.getMessage());
ErrorResponse error = ErrorResponse.builder()
.error("Service Error")
.message("Authentication service temporarily unavailable")
.timestamp(Instant.now())
.build();
return ResponseEntity.status(HttpStatus.SERVICE_UNAVAILABLE).body(error);
}
}
🚀 Production Considerations
1. Security Best Practices
- Token Rotation – Implement refresh token rotation
- Rate Limiting – Add rate limiting to auth endpoints
- Audit Logging – Log all authentication attempts
- Secret Management – Use secure secret storage
2. Performance Optimizations
- Connection Pooling – Configure RestTemplate with connection pool
- Caching – Cache user details for short periods
- Async Processing – Use async for non-critical operations
3. Monitoring and Metrics
@Component
public class AuthMetrics {
private final MeterRegistry meterRegistry;
private final Counter authSuccessCounter;
private final Counter authFailureCounter;
private final Timer authTimer;
public AuthMetrics(MeterRegistry meterRegistry) {
this.meterRegistry = meterRegistry;
this.authSuccessCounter = Counter.builder("auth.success")
.description("Successful authentications")
.register(meterRegistry);
this.authFailureCounter = Counter.builder("auth.failure")
.description("Failed authentications")
.register(meterRegistry);
this.authTimer = Timer.builder("auth.duration")
.description("Authentication duration")
.register(meterRegistry);
}
public void recordAuthSuccess() {
authSuccessCounter.increment();
}
public void recordAuthFailure() {
authFailureCounter.increment();
}
public Timer.Sample startTimer() {
return Timer.start(meterRegistry);
}
}
🚀 Continue Your Security Journey
🧭 Next Steps in Spring Security:
- ▶️ Spring Security Hub
- ▶️ Step 1: Architecture Foundation
- ▶️ Step 2: Configuration Fundamentals
- ▶️ Step 3: Implementation Strategy
- ✅ Step 4: Production Implementation (Current)
- ▶️ Step 5: Testing & Validation
Part of the AdventureTube technical blog series supporting the JavaiOS YouTube channel.