- Published on
Building Production-Ready Authentication Systems: A Complete Guide
- Authors
- Name
- Gary Huynh
- @gary_atruedev
Building Production-Ready Authentication Systems: A Complete Guide
Authentication is the gatekeeper of your application. Get it wrong, and you're either frustrating legitimate users or leaving your system vulnerable to attacks. In this comprehensive guide, I'll walk you through building a production-ready authentication system that balances security, usability, and maintainability.
Authentication vs Authorization: Getting the Basics Right
Before diving into implementation, let's clarify the fundamentals:
- Authentication: Verifying who a user is ("I am John")
- Authorization: Determining what a user can do ("John can edit posts")
Many developers conflate these concepts, leading to security vulnerabilities. Keep them separate in your design.
Choosing Your Authentication Strategy
Option 1: Session-Based Authentication
Traditional cookie-based sessions work well for:
- Server-rendered applications
- Single domain deployments
- Applications requiring strict session control
@RestController
@RequestMapping("/api/auth")
public class SessionAuthController {
@PostMapping("/login")
public ResponseEntity<?> login(@RequestBody LoginRequest request,
HttpSession session) {
// Validate credentials
User user = authService.authenticate(
request.getUsername(),
request.getPassword()
);
// Create session
session.setAttribute("userId", user.getId());
session.setAttribute("roles", user.getRoles());
return ResponseEntity.ok(new AuthResponse(user));
}
@PostMapping("/logout")
public ResponseEntity<?> logout(HttpSession session) {
session.invalidate();
return ResponseEntity.ok().build();
}
}
Option 2: Token-Based Authentication (JWT)
JWTs are ideal for:
- Stateless microservices
- Mobile applications
- Cross-domain scenarios
- Distributed systems
@Component
public class JwtTokenProvider {
@Value("${jwt.secret}")
private String jwtSecret;
@Value("${jwt.expiration}")
private int jwtExpiration;
public String generateToken(Authentication authentication) {
UserPrincipal userPrincipal = (UserPrincipal) authentication.getPrincipal();
Date expiryDate = new Date(System.currentTimeMillis() + jwtExpiration);
return Jwts.builder()
.setSubject(Long.toString(userPrincipal.getId()))
.setIssuedAt(new Date())
.setExpiration(expiryDate)
.claim("roles", userPrincipal.getAuthorities())
.signWith(SignatureAlgorithm.HS512, jwtSecret)
.compact();
}
public boolean validateToken(String authToken) {
try {
Jwts.parser().setSigningKey(jwtSecret).parseClaimsJws(authToken);
return true;
} catch (SignatureException ex) {
log.error("Invalid JWT signature");
} catch (MalformedJwtException ex) {
log.error("Invalid JWT token");
} catch (ExpiredJwtException ex) {
log.error("Expired JWT token");
} catch (UnsupportedJwtException ex) {
log.error("Unsupported JWT token");
} catch (IllegalArgumentException ex) {
log.error("JWT claims string is empty");
}
return false;
}
}
Implementing JWT-Based Authentication
Step 1: User Registration
@Service
@Transactional
public class AuthService {
private final UserRepository userRepository;
private final PasswordEncoder passwordEncoder;
private final JwtTokenProvider tokenProvider;
public AuthResponse registerUser(SignUpRequest signUpRequest) {
// Check if username exists
if (userRepository.existsByUsername(signUpRequest.getUsername())) {
throw new BadRequestException("Username already taken!");
}
// Check if email exists
if (userRepository.existsByEmail(signUpRequest.getEmail())) {
throw new BadRequestException("Email already in use!");
}
// Create new user
User user = new User();
user.setUsername(signUpRequest.getUsername());
user.setEmail(signUpRequest.getEmail());
user.setPassword(passwordEncoder.encode(signUpRequest.getPassword()));
user.setRoles(Collections.singleton(Role.ROLE_USER));
User savedUser = userRepository.save(user);
// Generate token
String token = tokenProvider.generateToken(
new UsernamePasswordAuthenticationToken(
savedUser.getId(),
null,
savedUser.getAuthorities()
)
);
return new AuthResponse(token, savedUser);
}
}
Step 2: Secure Password Storage
Never store passwords in plain text. Use a strong hashing algorithm:
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder(12); // 12 rounds
}
@Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
}
Step 3: JWT Authentication Filter
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
@Autowired
private JwtTokenProvider tokenProvider;
@Autowired
private CustomUserDetailsService customUserDetailsService;
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain)
throws ServletException, IOException {
try {
String jwt = getJwtFromRequest(request);
if (StringUtils.hasText(jwt) && tokenProvider.validateToken(jwt)) {
Long userId = tokenProvider.getUserIdFromJWT(jwt);
UserDetails userDetails = customUserDetailsService.loadUserById(userId);
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(
userDetails,
null,
userDetails.getAuthorities()
);
authentication.setDetails(
new WebAuthenticationDetailsSource().buildDetails(request)
);
SecurityContextHolder.getContext().setAuthentication(authentication);
}
} catch (Exception ex) {
logger.error("Could not set user authentication in security context", ex);
}
filterChain.doFilter(request, response);
}
private String getJwtFromRequest(HttpServletRequest request) {
String bearerToken = request.getHeader("Authorization");
if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
return bearerToken.substring(7);
}
return null;
}
}
Session Management Best Practices
1. Implement Refresh Tokens
Don't make your access tokens long-lived. Use refresh tokens:
@Entity
@Table(name = "refresh_tokens")
public class RefreshToken {
@Id
@GeneratedValue(strategy = GenerationType.UUID)
private String id;
@OneToOne
@JoinColumn(name = "user_id", referencedColumnName = "id")
private User user;
@Column(nullable = false, unique = true)
private String token;
@Column(nullable = false)
private Instant expiryDate;
// getters and setters
}
@Service
public class RefreshTokenService {
@Value("${jwt.refresh.expiration}")
private Long refreshTokenDurationMs;
@Autowired
private RefreshTokenRepository refreshTokenRepository;
public RefreshToken createRefreshToken(Long userId) {
RefreshToken refreshToken = new RefreshToken();
refreshToken.setUser(userRepository.findById(userId).get());
refreshToken.setExpiryDate(Instant.now().plusMillis(refreshTokenDurationMs));
refreshToken.setToken(UUID.randomUUID().toString());
refreshToken = refreshTokenRepository.save(refreshToken);
return refreshToken;
}
public RefreshToken verifyExpiration(RefreshToken token) {
if (token.getExpiryDate().compareTo(Instant.now()) < 0) {
refreshTokenRepository.delete(token);
throw new TokenRefreshException(token.getToken(),
"Refresh token was expired. Please make a new signin request");
}
return token;
}
}
2. Implement Logout Properly
For JWT-based systems, maintain a token blacklist:
@Service
public class TokenBlacklistService {
private final RedisTemplate<String, String> redisTemplate;
public void blacklistToken(String token, long expirationTime) {
redisTemplate.opsForValue().set(
"blacklist_" + token,
"true",
expirationTime,
TimeUnit.MILLISECONDS
);
}
public boolean isBlacklisted(String token) {
return redisTemplate.hasKey("blacklist_" + token);
}
}
OAuth 2.0 and Social Login
Implementing Google OAuth
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private CustomOAuth2UserService customOAuth2UserService;
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.cors()
.and()
.csrf()
.disable()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
.antMatchers("/auth/**", "/oauth2/**")
.permitAll()
.anyRequest()
.authenticated()
.and()
.oauth2Login()
.authorizationEndpoint()
.baseUri("/oauth2/authorize")
.authorizationRequestRepository(cookieAuthorizationRequestRepository())
.and()
.redirectionEndpoint()
.baseUri("/oauth2/callback/*")
.and()
.userInfoEndpoint()
.userService(customOAuth2UserService)
.and()
.successHandler(oAuth2AuthenticationSuccessHandler)
.failureHandler(oAuth2AuthenticationFailureHandler);
http.addFilterBefore(tokenAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
}
}
Custom OAuth2 User Service
@Service
public class CustomOAuth2UserService extends DefaultOAuth2UserService {
@Autowired
private UserRepository userRepository;
@Override
public OAuth2User loadUser(OAuth2UserRequest oAuth2UserRequest) throws OAuth2AuthenticationException {
OAuth2User oAuth2User = super.loadUser(oAuth2UserRequest);
try {
return processOAuth2User(oAuth2UserRequest, oAuth2User);
} catch (AuthenticationException ex) {
throw ex;
} catch (Exception ex) {
throw new InternalAuthenticationServiceException(ex.getMessage(), ex.getCause());
}
}
private OAuth2User processOAuth2User(OAuth2UserRequest oAuth2UserRequest, OAuth2User oAuth2User) {
OAuth2UserInfo oAuth2UserInfo = OAuth2UserInfoFactory.getOAuth2UserInfo(
oAuth2UserRequest.getClientRegistration().getRegistrationId(),
oAuth2User.getAttributes()
);
if(StringUtils.isEmpty(oAuth2UserInfo.getEmail())) {
throw new OAuth2AuthenticationProcessingException("Email not found from OAuth2 provider");
}
Optional<User> userOptional = userRepository.findByEmail(oAuth2UserInfo.getEmail());
User user;
if(userOptional.isPresent()) {
user = userOptional.get();
if(!user.getProvider().equals(AuthProvider.valueOf(
oAuth2UserRequest.getClientRegistration().getRegistrationId()))) {
throw new OAuth2AuthenticationProcessingException("You're signed up with " +
user.getProvider() + " account. Please use your " + user.getProvider() +
" account to login.");
}
user = updateExistingUser(user, oAuth2UserInfo);
} else {
user = registerNewUser(oAuth2UserRequest, oAuth2UserInfo);
}
return UserPrincipal.create(user, oAuth2User.getAttributes());
}
}
Security Considerations
1. Rate Limiting
Prevent brute force attacks:
@Component
public class RateLimitingFilter extends OncePerRequestFilter {
private final RateLimiter rateLimiter = RateLimiter.create(10.0); // 10 requests per second
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain)
throws ServletException, IOException {
if (request.getRequestURI().startsWith("/auth/")) {
if (!rateLimiter.tryAcquire()) {
response.setStatus(HttpStatus.TOO_MANY_REQUESTS.value());
response.getWriter().write("Too many requests");
return;
}
}
filterChain.doFilter(request, response);
}
}
2. Account Lockout
Implement progressive delays after failed attempts:
@Service
public class LoginAttemptService {
private final int MAX_ATTEMPT = 5;
private LoadingCache<String, Integer> attemptsCache;
public LoginAttemptService() {
super();
attemptsCache = CacheBuilder.newBuilder()
.expireAfterWrite(1, TimeUnit.DAYS)
.build(new CacheLoader<String, Integer>() {
@Override
public Integer load(String key) {
return 0;
}
});
}
public void loginFailed(String key) {
int attempts = 0;
try {
attempts = attemptsCache.get(key);
} catch (ExecutionException e) {
attempts = 0;
}
attempts++;
attemptsCache.put(key, attempts);
}
public boolean isBlocked(String key) {
try {
return attemptsCache.get(key) >= MAX_ATTEMPT;
} catch (ExecutionException e) {
return false;
}
}
}
3. CSRF Protection
For session-based auth, always use CSRF tokens:
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf()
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
.and()
// ... other configuration
}
}
Authentication at Scale
When your system grows from thousands to millions of users, authentication becomes one of the most critical performance bottlenecks. Here's how to scale authentication effectively.
Distributed Session Management Architecture
The Challenge: At scale, session management can consume 40-60% of your Redis cluster capacity if not properly architected.
@Configuration
public class DistributedSessionConfig {
// Partition sessions across multiple Redis nodes
@Bean
public RedisTemplate<String, Object> sessionRedisTemplate(
@Value("${redis.session.nodes}") String[] nodes) {
RedisClusterConfiguration clusterConfig = new RedisClusterConfiguration(
Arrays.asList(nodes)
);
// Enable read from replicas for better performance
LettuceClientConfiguration clientConfig = LettuceClientConfiguration.builder()
.readFrom(ReadFrom.REPLICA_PREFERRED)
.commandTimeout(Duration.ofMillis(500))
.build();
LettuceConnectionFactory factory = new LettuceConnectionFactory(
clusterConfig, clientConfig
);
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(factory);
// Use Kryo for 3x better serialization performance
template.setDefaultSerializer(new KryoRedisSerializer());
return template;
}
@Bean
public SessionRegistry distributedSessionRegistry() {
return new RedisBackedSessionRegistry(sessionRedisTemplate()) {
@Override
protected String computeSessionKey(String sessionId) {
// Distribute sessions across shards based on user ID
int shard = sessionId.hashCode() % 16;
return String.format("session:shard:%d:%s", shard, sessionId);
}
};
}
}
Performance Benchmarks:
# Session Storage Performance Comparison (1M concurrent sessions)
storage_type:
in_memory:
read_latency_p99: 0.1ms
write_latency_p99: 0.2ms
max_capacity: 10k sessions
failover: none
redis_standalone:
read_latency_p99: 1ms
write_latency_p99: 2ms
max_capacity: 100k sessions
failover: sentinel
redis_cluster:
read_latency_p99: 2ms
write_latency_p99: 3ms
max_capacity: 10M sessions
failover: automatic
dynamodb:
read_latency_p99: 10ms
write_latency_p99: 15ms
max_capacity: unlimited
failover: multi-region
Zero-Trust Architecture Implementation
In modern distributed systems, zero-trust is not optional—it's essential.
@Component
public class ZeroTrustAuthenticationManager {
private final DeviceTrustEvaluator deviceTrust;
private final NetworkContextEvaluator networkContext;
private final BehaviorAnalyzer behaviorAnalyzer;
private final RiskScoreCalculator riskCalculator;
public AuthenticationDecision authenticate(AuthenticationRequest request) {
// Never trust, always verify
RiskProfile risk = RiskProfile.builder()
.deviceTrust(deviceTrust.evaluate(request.getDeviceFingerprint()))
.networkRisk(networkContext.evaluate(request.getClientIP()))
.behaviorScore(behaviorAnalyzer.analyze(request.getUserId()))
.build();
double riskScore = riskCalculator.calculate(risk);
if (riskScore > 0.8) {
// High risk - deny or require step-up authentication
return AuthenticationDecision.stepUpRequired(
StepUpMethod.HARDWARE_TOKEN
);
} else if (riskScore > 0.5) {
// Medium risk - require MFA
return AuthenticationDecision.mfaRequired();
}
// Low risk - proceed with continuous verification
return AuthenticationDecision.allow()
.withContinuousVerification(Duration.ofMinutes(15));
}
@EventListener
public void onSecurityEvent(SecurityEvent event) {
// Real-time risk adjustment
if (event.getType() == SecurityEventType.ANOMALY_DETECTED) {
// Immediately revoke all sessions for affected user
sessionRegistry.revokeAllSessions(event.getUserId());
// Notify security team
securityAlertService.alert(event);
}
}
}
Multi-Factor Authentication Patterns at Scale
Adaptive MFA Based on Risk Scoring:
@Service
public class AdaptiveMFAService {
private final Map<MFAMethod, MFAProvider> providers = new ConcurrentHashMap<>();
@PostConstruct
public void initProviders() {
// Register providers in order of security strength
providers.put(MFAMethod.SMS, new SMSProvider()); // Weakest
providers.put(MFAMethod.TOTP, new TOTPProvider());
providers.put(MFAMethod.PUSH, new PushNotificationProvider());
providers.put(MFAMethod.WEBAUTHN, new WebAuthnProvider());
providers.put(MFAMethod.HARDWARE_TOKEN, new HardwareTokenProvider()); // Strongest
}
public MFAChallenge createChallenge(User user, RiskScore risk) {
// Select MFA method based on risk
List<MFAMethod> requiredMethods = new ArrayList<>();
if (risk.getValue() > 0.9) {
// Ultra high risk - require multiple factors
requiredMethods.add(MFAMethod.HARDWARE_TOKEN);
requiredMethods.add(MFAMethod.PUSH);
} else if (risk.getValue() > 0.7) {
// High risk - strong factor required
requiredMethods.add(user.hasWebAuthn() ?
MFAMethod.WEBAUTHN : MFAMethod.TOTP);
} else if (risk.getValue() > 0.3) {
// Medium risk - any registered factor
requiredMethods.add(user.getPreferredMFAMethod());
}
// Low risk - no MFA required
return MFAChallenge.builder()
.userId(user.getId())
.requiredMethods(requiredMethods)
.expiresIn(Duration.ofMinutes(5))
.build();
}
}
// WebAuthn implementation for strongest security
@Component
public class WebAuthnProvider implements MFAProvider {
private final RelyingParty relyingParty;
@Override
public AuthenticationChallenge createChallenge(User user) {
// Generate challenge for hardware security key
PublicKeyCredentialRequestOptions options =
PublicKeyCredentialRequestOptions.builder()
.challenge(generateChallenge())
.rpId(relyingParty.getId())
.userVerification(UserVerificationRequirement.REQUIRED)
.timeout(60000L)
.allowCredentials(user.getWebAuthnCredentials())
.build();
return new WebAuthnChallenge(options);
}
}
Federated Identity Management
Enterprise-Grade SAML/OIDC Integration:
@Configuration
@EnableFederatedIdentity
public class FederatedIdentityConfig {
@Bean
public IdentityProviderRegistry identityProviderRegistry() {
return new DynamicIdentityProviderRegistry() {
@Override
protected void configure() {
// Support multiple identity providers per tenant
registerProvider("okta", OktaSAMLProvider.class);
registerProvider("auth0", Auth0OIDCProvider.class);
registerProvider("azure-ad", AzureADProvider.class);
registerProvider("ping", PingFederateProvider.class);
}
};
}
@Bean
public FederatedAuthenticationManager federatedAuthManager() {
return new FederatedAuthenticationManager() {
@Override
public Authentication authenticate(FederatedAuthRequest request) {
// Tenant-specific IdP resolution
Tenant tenant = tenantResolver.resolve(request);
IdentityProvider idp = identityProviderRegistry
.getProvider(tenant.getIdentityProvider());
// Validate SAML assertion or OIDC token
FederatedIdentity identity = idp.validateAssertion(
request.getAssertion()
);
// Map external identity to internal user
User user = userMappingService.mapOrCreate(
identity,
tenant,
MappingStrategy.JIT_PROVISIONING
);
// Apply tenant-specific policies
applyTenantPolicies(user, tenant);
return new FederatedAuthentication(user, identity);
}
};
}
}
// Just-In-Time user provisioning
@Service
public class JITUserProvisioningService {
@Transactional
public User provisionUser(FederatedIdentity identity, Tenant tenant) {
// Create user with federated identity
User user = User.builder()
.email(identity.getEmail())
.username(generateUsername(identity, tenant))
.federatedId(identity.getSubject())
.identityProvider(identity.getProvider())
.tenant(tenant)
.build();
// Apply tenant-specific attributes
Map<String, Object> attributes = identity.getAttributes();
if (tenant.getAttributeMapping() != null) {
mapTenantAttributes(user, attributes, tenant.getAttributeMapping());
}
// Assign default roles based on IdP groups
Set<Role> roles = roleMapper.mapFromGroups(
identity.getGroups(),
tenant.getRoleMapping()
);
user.setRoles(roles);
return userRepository.save(user);
}
}
Performance Benchmarks for Authentication Strategies
@Component
public class AuthenticationBenchmarkService {
public BenchmarkResults runBenchmarks() {
return BenchmarkResults.builder()
.scenario("10k concurrent authentications")
.results(Map.of(
"jwt_validation", new BenchmarkResult(
"JWT Token Validation",
0.5, // ms p50
1.2, // ms p99
15000 // ops/sec/core
),
"session_lookup", new BenchmarkResult(
"Redis Session Lookup",
1.0, // ms p50
3.5, // ms p99
8000 // ops/sec/core
),
"oauth_token_exchange", new BenchmarkResult(
"OAuth Token Exchange",
50, // ms p50
150, // ms p99
200 // ops/sec/core
),
"saml_assertion_validation", new BenchmarkResult(
"SAML Assertion Validation",
5, // ms p50
15, // ms p99
2000 // ops/sec/core
),
"webauthn_verification", new BenchmarkResult(
"WebAuthn Verification",
10, // ms p50
25, // ms p99
1000 // ops/sec/core
)
))
.recommendations(List.of(
"Use JWT for stateless services (highest throughput)",
"Cache SAML metadata (10x performance improvement)",
"Implement session affinity for 30% latency reduction",
"Use connection pooling for Redis (2x throughput)"
))
.build();
}
}
Compliance Mapping
@Component
public class ComplianceMapper {
public ComplianceReport mapAuthenticationToCompliance(
AuthenticationConfig config) {
Map<ComplianceStandard, ComplianceStatus> mapping = new HashMap<>();
// SOC 2 Type II
mapping.put(ComplianceStandard.SOC2_TYPE_II,
evaluateSOC2(config));
// HIPAA
mapping.put(ComplianceStandard.HIPAA,
evaluateHIPAA(config));
// PCI DSS
mapping.put(ComplianceStandard.PCI_DSS,
evaluatePCIDSS(config));
// GDPR
mapping.put(ComplianceStandard.GDPR,
evaluateGDPR(config));
return new ComplianceReport(mapping);
}
private ComplianceStatus evaluateSOC2(AuthenticationConfig config) {
List<ComplianceRequirement> requirements = List.of(
new ComplianceRequirement(
"CC6.1",
"Logical and Physical Access Controls",
config.hasMFA() && config.hasRBAC()
),
new ComplianceRequirement(
"CC6.7",
"User Authentication via Unique Login",
config.hasUniqueUserIdentification()
),
new ComplianceRequirement(
"CC6.8",
"Failed Login Attempt Monitoring",
config.hasLoginAttemptMonitoring()
)
);
return ComplianceStatus.evaluate(requirements);
}
private ComplianceStatus evaluateHIPAA(AuthenticationConfig config) {
List<ComplianceRequirement> requirements = List.of(
new ComplianceRequirement(
"164.312(a)(1)",
"Access Control - Unique User Identification",
config.hasUniqueUserIdentification()
),
new ComplianceRequirement(
"164.312(a)(2)",
"Automatic Logoff",
config.getSessionTimeout() <= Duration.ofMinutes(30)
),
new ComplianceRequirement(
"164.312(d)",
"Person or Entity Authentication",
config.hasMFA() || config.hasBiometric()
)
);
return ComplianceStatus.evaluate(requirements);
}
}
Scaling Authentication
@Configuration
@EnableRedisHttpSession
public class SessionConfig {
@Bean
public LettuceConnectionFactory connectionFactory() {
return new LettuceConnectionFactory(
new RedisStandaloneConfiguration("localhost", 6379)
);
}
@Bean
public HttpSessionIdResolver httpSessionIdResolver() {
return HeaderHttpSessionIdResolver.xAuthToken();
}
}
2. JWT Key Rotation
Implement key rotation for enhanced security:
@Component
public class JwtKeyRotationService {
private final Map<String, Key> keyStore = new ConcurrentHashMap<>();
private volatile String currentKeyId;
@PostConstruct
public void init() {
rotateKey();
}
@Scheduled(cron = "0 0 0 * * ?") // Daily at midnight
public void rotateKey() {
String newKeyId = UUID.randomUUID().toString();
Key newKey = Keys.secretKeyFor(SignatureAlgorithm.HS512);
keyStore.put(newKeyId, newKey);
currentKeyId = newKeyId;
// Keep last 3 keys for validation of existing tokens
if (keyStore.size() > 3) {
String oldestKeyId = keyStore.keySet().iterator().next();
keyStore.remove(oldestKeyId);
}
}
public String getCurrentKeyId() {
return currentKeyId;
}
public Key getKey(String keyId) {
return keyStore.get(keyId);
}
}
Testing Your Auth System
Integration Tests
@SpringBootTest
@AutoConfigureMockMvc
@TestPropertySource(locations = "classpath:application-test.properties")
public class AuthControllerIntegrationTest {
@Autowired
private MockMvc mockMvc;
@Autowired
private UserRepository userRepository;
@Test
public void testSuccessfulLogin() throws Exception {
// Create test user
User user = new User("testuser", "test@example.com",
passwordEncoder.encode("password123"));
userRepository.save(user);
LoginRequest loginRequest = new LoginRequest("testuser", "password123");
mockMvc.perform(post("/auth/login")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(loginRequest)))
.andExpect(status().isOk())
.andExpect(jsonPath("$.accessToken").exists())
.andExpect(jsonPath("$.tokenType").value("Bearer"));
}
@Test
public void testLoginWithInvalidCredentials() throws Exception {
LoginRequest loginRequest = new LoginRequest("invalid", "wrong");
mockMvc.perform(post("/auth/login")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(loginRequest)))
.andExpect(status().isUnauthorized());
}
}
Security Tests
@Test
public void testRateLimiting() throws Exception {
LoginRequest loginRequest = new LoginRequest("user", "pass");
// Make 11 requests (limit is 10)
for (int i = 0; i < 11; i++) {
ResultActions result = mockMvc.perform(post("/auth/login")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(loginRequest)));
if (i < 10) {
result.andExpect(status().is4xxError());
} else {
result.andExpect(status().isTooManyRequests());
}
}
}
Common Pitfalls and How to Avoid Them
1. Storing Sensitive Data in JWTs
Don't do this:
// BAD: Sensitive data in JWT
claims.put("creditCard", user.getCreditCardNumber());
claims.put("ssn", user.getSocialSecurityNumber());
Do this instead:
// GOOD: Only non-sensitive identifiers
claims.put("userId", user.getId());
claims.put("roles", user.getRoles());
2. Not Validating Token Signature
Always validate:
public boolean validateToken(String authToken) {
try {
Jwts.parser()
.setSigningKey(jwtSecret)
.parseClaimsJws(authToken);
return true;
} catch (SignatureException ex) {
log.error("Invalid JWT signature");
return false;
}
// ... handle other exceptions
}
3. Using Weak Passwords
Implement password policies:
@Component
public class PasswordValidator {
private static final String PASSWORD_PATTERN =
"^(?=.*[0-9])(?=.*[a-z])(?=.*[A-Z])(?=.*[@#$%^&+=])(?=\\S+$).{8,}$";
private final Pattern pattern = Pattern.compile(PASSWORD_PATTERN);
public boolean validate(String password) {
return pattern.matcher(password).matches();
}
}
Production Checklist
Before going to production, ensure:
- [ ] Security Headers are configured
@Configuration
public class SecurityHeadersConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.headers()
.frameOptions().deny()
.xssProtection().and()
.contentSecurityPolicy("default-src 'self'");
return http.build();
}
}
- [ ] HTTPS Only - Redirect all HTTP to HTTPS
- [ ] Secure Cookies - HttpOnly and Secure flags
- [ ] Rate Limiting - On all authentication endpoints
- [ ] Monitoring - Track failed login attempts
- [ ] Backup Authentication - Have a fallback method
- [ ] Token Expiration - Short-lived access tokens
- [ ] Audit Logging - Log all auth events
- [ ] Error Messages - Don't leak information
- [ ] Database Indexes - On username/email fields
- [ ] Load Testing - Verify system can handle load
Conclusion
Building a production-ready authentication system requires careful consideration of security, scalability, and user experience. The implementation details matter—from proper password hashing to token validation to session management.
Remember: authentication is not a feature you implement once and forget. It requires ongoing maintenance, security updates, and monitoring. Stay informed about new vulnerabilities and best practices.
Next Steps
- Implement the authentication system step by step
- Add comprehensive logging and monitoring
- Conduct security audits regularly
- Stay updated with OWASP guidelines
- Consider using established libraries like Spring Security
Have questions about authentication implementation? Found a security issue in this guide? Let me know in the comments below or reach out on Twitter.