AuthProvider/OAuthProvider/LocalAuthProvider/TokenService via Strategy so multiple auth providers coexist, and session vs token design choices mapped directly onto Spring Security's own internals.
Published September 23, 2026
interface AuthProvider {
AuthResult authenticate(Credentials credentials);
}
class LocalAuthProvider implements AuthProvider {
public AuthResult authenticate(Credentials credentials) {
User user = userRepository.findByEmail(credentials.getEmail());
if (user == null || !passwordEncoder.matches(credentials.getPassword(), user.getPasswordHash())) {
return AuthResult.failure("Invalid credentials");
}
return AuthResult.success(user);
}
}
class OAuthProvider implements AuthProvider {
public AuthResult authenticate(Credentials credentials) {
// exchanges an OAuth authorization code / token with the external provider, then resolves/creates a local User
OAuthTokenResponse tokens = oauthClient.exchangeCode(credentials.getAuthCode());
User user = findOrCreateUserFromOAuthProfile(tokens);
return AuthResult.success(user);
}
}
class AuthenticationService {
private final Map<AuthMethod, AuthProvider> providers; // email/password, Google OAuth, GitHub OAuth, etc.
AuthResult login(AuthMethod method, Credentials credentials) {
AuthProvider provider = providers.get(method);
return provider.authenticate(credentials);
}
}
This is the same Strategy-per-implementation shape as Spring Security's own ProviderManager delegating to multiple AuthenticationProviders (see Authentication Mechanics) — an application supporting both local password login and "Sign in with Google" doesn't branch internally on which method was used; it looks up the right AuthProvider and delegates, exactly matching the real framework's own architecture.
class TokenService {
String issueToken(User user) {
return jwtBuilder.subject(user.getId()).claim("roles", user.getRoles()).expiry(Duration.ofMinutes(15)).sign();
}
Optional<User> validateToken(String token) {
if (!jwtVerifier.isValid(token)) return Optional.empty();
return userRepository.findById(jwtVerifier.getSubject(token));
}
}
This is the direct connection point to production Spring Security, worth stating explicitly:
AuthenticationService.login() create a server-side session (analogous to what SecurityContextHolder's default ThreadLocal-backed context does per-request once populated by the filter chain — see Authentication Mechanics), with a session ID cookie referencing it.TokenService above implements) matches JWT-Based Stateless Auth's custom OncePerRequestFilter directly — validateToken() here is functionally the same operation that filter performs on every incoming request to populate SecurityContextHolder.Recognizing that this LLD exercise's AuthProvider/TokenService classes are a simplified re-derivation of what Spring Security's AuthenticationProvider/JWT filter machinery already does is exactly the kind of LLD-to-framework connection this course has built toward — the goal isn't reinventing Spring Security, it's understanding it well enough to explain why it's built the way it is.
Q: Why does OAuthProvider need to 'find or create' a local User, rather than just trusting the OAuth provider's identity directly? A: Most applications need their own User record (for authorization roles, application-specific data, foreign keys from other tables) regardless of how the user originally authenticated — OAuth establishes IDENTITY, but the application still needs its own representation of that identity to attach permissions and data to, which is exactly why find-or-create-on-first-login is the standard pattern.
Q: How would this design support a user who's authenticated via OAuth wanting to ALSO set a local password later? A: This argues for User supporting multiple linked auth methods (a separate AuthMethod-to-User mapping table, rather than assuming exactly one auth method per user) — a real product decision worth surfacing explicitly rather than assuming the simpler one-auth-method-per-user model always holds.
Q: Should password verification (passwordEncoder.matches()) happen inside LocalAuthProvider or a separate service? A: Keeping it inside LocalAuthProvider (as shown) is reasonable since password matching is specific to that one auth method — a shared AuthenticationService wouldn't have anywhere else meaningful to put OAuth-specific token exchange logic either, so each provider owning its own method-specific verification logic keeps the Strategy separation clean.
Q: Why 15 minutes for token expiry in this example specifically? A: An arbitrary but realistic short-lived access-token duration matching JWT-Based Stateless Auth's own 'short-lived access token, longer-lived refresh token' pattern — worth stating that the SPECIFIC number is a tunable business/security tradeoff (shorter = more secure against token theft, more refresh overhead; longer = the reverse), not a fixed correct value.