Authentication & Authorization
Authentication (AuthN): Who are you? Authorization (AuthZ): What are you allowed to do?
These are separate concerns. A user can be authenticated (valid JWT) but not authorized (403 on a specific resource).
| HTTP Status | Meaning |
|---|---|
401 Unauthorized | Not authenticated โ identity not established |
403 Forbidden | Authenticated but not authorized for this resource |
Session-Based Authenticationโ
1. User submits credentials โ Server validates
2. Server creates session in store (Redis/DB)
3. Server sends Set-Cookie: SESSIONID=abc123 (HttpOnly, Secure, SameSite)
4. Client sends cookie on every request automatically
5. Server looks up session in store โ extracts user context
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
.sessionFixation().changeSessionId() // Prevent session fixation
.maximumSessions(1)
.maxSessionsPreventsLogin(false) // New login kicks old session
)
.rememberMe(remember -> remember
.tokenRepository(persistentTokenRepository())
.tokenValiditySeconds(7 * 24 * 3600) // 7 days
)
.build();
}
Pros: Easy to revoke (delete session). Full server control over expiry. Cons: Horizontal scaling requires shared session store (Redis). Stateful.
Token-Based Authentication (JWT)โ
1. User submits credentials
2. Server validates โ issues JWT (signed with private key)
3. Client stores JWT (memory > httpOnly cookie > localStorage)
4. Client sends: Authorization: Bearer <jwt> on every request
5. Server validates signature โ no DB lookup needed (stateless)
JWT Structureโ
eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImtleS0yMDI0LTAxIn0
.eyJpc3MiOiJodHRwczovL2F1dGguZXhhbXBsZS5jb20iLCJzdWIiOiJ1c2VyLTEyMzQ1In0
.SIGNATURE
Each section is Base64Url encoded:
// HEADER
{
"alg": "RS256",
"typ": "JWT",
"kid": "key-2024-01" // โ Key ID used to look up public key in JWKS
}
// PAYLOAD (claims)
{
"iss": "https://auth.example.com", // Issuer
"sub": "user-12345", // Subject
"aud": "https://api.example.com", // Audience
"exp": 1700003600, // Expiration (Unix timestamp)
"iat": 1700000000, // Issued at
"jti": "unique-token-id", // JWT ID (for revocation)
"roles": ["ROLE_USER"],
}
// SIGNATURE โ computed as:
// Base64Url(RS256_sign(privateKey, Base64Url(header) + "." + Base64Url(payload)))
JWT payload is only Base64Url encoded โ anyone can decode it. Never put passwords, secrets, or sensitive PII in JWT payload unless using JWE (JSON Web Encryption).
Signing Algorithmsโ
| Algorithm | Type | Key | Recommended Use |
|---|---|---|---|
HS256 | Symmetric HMAC | Shared secret | Single-service only; secret must not leak |
RS256 | Asymmetric RSA | Private + public key pair | Multi-service; preferred |
ES256 | Asymmetric ECDSA | Private + public key pair | Better performance than RSA, same security |
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http
.csrf(AbstractHttpConfigurer::disable)
.sessionManagement(s -> s.sessionCreationPolicy(STATELESS))
.authorizeHttpRequests(auth -> auth
.requestMatchers("/actuator/health").permitAll()
.requestMatchers(HttpMethod.GET, "/api/products/**").permitAll()
.requestMatchers("/api/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()
)
.oauth2ResourceServer(oauth2 -> oauth2
.jwt(jwt -> jwt
.decoder(jwtDecoder())
.jwtAuthenticationConverter(jwtAuthConverter())
)
)
.build();
}
@Bean
public JwtDecoder jwtDecoder() {
// Automatically fetches public keys from JWKS endpoint
// Handles kid lookup and key rotation transparently
return JwtDecoders.fromIssuerLocation("https://auth.example.com");
}
@Bean
public JwtAuthenticationConverter jwtAuthConverter() {
JwtGrantedAuthoritiesConverter converter = new JwtGrantedAuthoritiesConverter();
converter.setAuthoritiesClaimName("roles");
converter.setAuthorityPrefix("ROLE_");
JwtAuthenticationConverter jwtConverter = new JwtAuthenticationConverter();
jwtConverter.setJwtGrantedAuthoritiesConverter(converter);
return jwtConverter;
}
}
Access Token + Refresh Token Patternโ
Access Token: Short-lived (5โ15 min) โ sent on every API request
Refresh Token: Long-lived (7โ30 days) โ sent ONLY to /auth/refresh
Flow:
Login โ { access_token (15min), refresh_token (30 days) }
API calls use access_token
access_token expires โ POST /auth/refresh with refresh_token โ new access_token
refresh_token expires โ user must log in again
@PostMapping("/auth/refresh")
public TokenResponse refresh(@RequestBody RefreshRequest req) {
RefreshToken token = refreshTokenRepository
.findByToken(req.getRefreshToken())
.orElseThrow(() -> new InvalidTokenException("Invalid refresh token"));
if (token.isExpired()) {
refreshTokenRepository.delete(token);
throw new InvalidTokenException("Refresh token expired");
}
// ROTATE: issue new refresh token, invalidate old one
// If old token is used again โ theft detected โ lock account
refreshTokenRepository.delete(token);
String newRefreshToken = UUID.randomUUID().toString();
refreshTokenRepository.save(new RefreshToken(token.getUserId(), newRefreshToken,
Instant.now().plus(30, ChronoUnit.DAYS)));
return new TokenResponse(
jwtService.generateAccessToken(token.getUserId()),
newRefreshToken
);
}
OAuth 2.0 Flowsโ
Authorization Code Flow + PKCE (Most Secure โ Web & Mobile)โ
1. App generates: code_verifier (random) + code_challenge = SHA256(code_verifier)
2. Redirect to: GET /authorize
?response_type=code
&client_id=CLIENT_ID
&redirect_uri=https://app.example.com/callback
&scope=openid profile email
&state=RANDOM_CSRF_TOKEN
&code_challenge=BASE64URL(SHA256(code_verifier))
&code_challenge_method=S256
3. User authenticates + consents at IdP
4. IdP redirects: https://app.example.com/callback?code=AUTH_CODE&state=SAME_STATE
5. App verifies state, then exchanges code:
POST /token
grant_type=authorization_code
&code=AUTH_CODE
&code_verifier=ORIGINAL_VERIFIER โ IdP hashes and compares to challenge
&redirect_uri=...
6. Response: { access_token, refresh_token, id_token, expires_in }
PKCE protects against: Authorization code interception โ even if the code is stolen, attacker cannot exchange it without the code_verifier.
Client Credentials Flow (Machine-to-Machine)โ
// Service A authenticates as itself (no user involved)
POST /token
grant_type=client_credentials
&client_id=SERVICE_A_ID
&client_secret=SERVICE_A_SECRET
&scope=read:orders
// Response: { access_token, expires_in }
// Service A โ GET /orders (Authorization: Bearer access_token)
OpenID Connect (OIDC)โ
OAuth 2.0 = Authorization (can access resources)
OIDC = Authentication (who the user is) โ built on top of OAuth 2.0
OIDC adds id_token โ a JWT with user identity claims:
{
"sub": "user-12345",
"email": "[email protected]",
"name": "Alice Smith",
"email_verified": true,
"iat": ..., "exp": ...
}
Rule: Use access_token to call APIs. Use id_token to establish user identity in your app.
Multi-Factor Authentication (MFA)โ
| Factor | Type | Examples |
|---|---|---|
| Something you know | Knowledge | Password, PIN |
| Something you have | Possession | TOTP app, hardware key (YubiKey), SMS OTP |
| Something you are | Inherence | Fingerprint, Face ID |
TOTP (RFC 6238 โ Google Authenticator)โ
Secret key shared during setup (shown as QR code)
OTP = HMAC-SHA1(secret, floor(Unix_timestamp / 30)) truncated to 6 digits
Valid for 30-second window (ยฑ1 window tolerance for clock skew)
@Service
public class TotpService {
private static final int WINDOW = 1;
public String generateSecret() {
byte[] buffer = new byte[20];
new SecureRandom().nextBytes(buffer);
return Base32.encode(buffer);
}
public boolean verifyCode(String secret, int userCode) {
long currentStep = Instant.now().getEpochSecond() / 30;
for (int i = -WINDOW; i <= WINDOW; i++) {
if (calculateTotp(secret, currentStep + i) == userCode) return true;
}
return false;
}
private int calculateTotp(String secret, long step) {
byte[] key = Base32.decode(secret);
byte[] msg = ByteBuffer.allocate(8).putLong(step).array();
byte[] hash = new HmacUtils(HmacAlgorithms.HMAC_SHA_1, key).hmac(msg);
int offset = hash[hash.length - 1] & 0x0f;
return ((hash[offset] & 0x7f) << 24
| (hash[offset+1] & 0xff) << 16
| (hash[offset+2] & 0xff) << 8
| (hash[offset+3] & 0xff)) % 1_000_000;
}
}
Passkeys (WebAuthn / FIDO2)โ
The modern passwordless standard โ phishing-resistant.
REGISTRATION:
1. Server sends challenge
2. Device creates public/private key pair (per origin)
3. Private key stays in device secure enclave โ NEVER leaves the device
4. Server stores: public key + credential ID
AUTHENTICATION:
1. Server sends challenge
2. User authenticates via biometric/PIN (unlocks secure enclave)
3. Device signs the challenge with private key
4. Server verifies signature using stored public key โ identity proven
Security properties:
โ
Phishing-resistant (keys are origin-bound)
โ
No password to steal or reuse
โ
No shared secret on server side
โ
Biometric stays on device
Authorization Modelsโ
RBAC (Role-Based Access Control)โ
User โ Role(s) โ Permission(s)
Roles: ADMIN, MANAGER, USER, GUEST
@PreAuthorize("hasRole('ADMIN')")
public void deleteUser(Long userId) { ... }
@PreAuthorize("hasAnyRole('ADMIN', 'MANAGER')")
public List<Order> getAllOrders() { ... }
// Ownership check inline
@PreAuthorize("hasRole('ADMIN') or #userId == authentication.principal.id")
public UserProfile getProfile(Long userId) { ... }
ABAC (Attribute-Based Access Control)โ
Policy: Allow if:
user.department == resource.department
AND user.clearanceLevel >= resource.sensitivityLevel
AND environment.time between 09:00 and 18:00
public class DocumentPermissionEvaluator implements PermissionEvaluator {
@Override
public boolean hasPermission(Authentication auth, Object target, Object permission) {
if (target instanceof Document doc) {
UserDetails user = (UserDetails) auth.getPrincipal();
return switch ((String) permission) {
case "READ" -> doc.getDepartment().equals(getUserDept(user))
|| hasRole(user, "ADMIN");
case "EDIT" -> doc.getOwnerId().equals(getUserId(user))
|| hasRole(user, "ADMIN");
case "DELETE" -> hasRole(user, "ADMIN");
default -> false;
};
}
return false;
}
}
@PreAuthorize("hasPermission(#document, 'EDIT')")
public void updateDocument(Document document) { ... }
Secure Cookie Flagsโ
| Flag | Effect |
|---|---|
HttpOnly | JavaScript cannot access cookie โ XSS protection |
Secure | Cookie only sent over HTTPS |
SameSite=Strict | Cookie not sent on any cross-site request โ strongest CSRF protection |
SameSite=Lax | Cookie not sent on cross-site POST โ sufficient for most apps |
SameSite=None; Secure | Cookie sent cross-site โ required for embedded/third-party apps |
Password Storageโ
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder(12); // Cost factor 12 โ 300ms per hash
}
| Algorithm | Status | Notes |
|---|---|---|
| MD5, SHA-1 | โ Broken | Reversible via rainbow tables |
| SHA-256 (unsalted) | โ Weak | GPU-crackable |
| BCrypt | โ Recommended | Adaptive cost, built-in salt |
| Argon2id | โ Best | Memory-hard, GPU-resistant |
| PBKDF2 | โ Acceptable | NIST-approved, FIPS contexts |
Interview Questionsโ
- What is the difference between authentication and authorization? What HTTP codes represent each failure?
- What are the pros and cons of JWT vs session-based authentication?
- Explain the OAuth 2.0 Authorization Code flow with PKCE. What does PKCE protect against?
- Why is
RS256preferred overHS256in a microservices architecture? - How do you implement token revocation with stateless JWTs?
- What is the difference between RBAC, ABAC, and ReBAC?
- How does TOTP (Google Authenticator) work?
- What are passkeys and how do they differ from passwords?
- Why should passwords be hashed with BCrypt instead of SHA-256?
- What cookie flags are required for secure session management?
- What is session fixation and how do you prevent it?
- How does the refresh token rotation pattern work and what attack does it detect?
- What is the difference between OAuth 2.0 and OIDC?
- What is the
kid(Key ID) claim in a JWT header used for?