Authentication & Authorization Overview¶
Authentication vs Authorization¶
Authentication Methods¶
Session-Based Authentication¶
Session Implementation¶
// Spring Session with Redis
@Configuration
@EnableRedisHttpSession(maxInactiveIntervalInSeconds = 1800) // 30 minutes
public class SessionConfig {
@Bean
public LettuceConnectionFactory connectionFactory() {
return new LettuceConnectionFactory(
new RedisStandaloneConfiguration("localhost", 6379));
}
}
// Session usage in controller
@PostMapping("/login")
public ResponseEntity<?> login(@RequestBody LoginRequest request,
HttpSession session) {
User user = authService.authenticate(request);
session.setAttribute("userId", user.getId());
session.setAttribute("roles", user.getRoles());
return ResponseEntity.ok().build();
}
@GetMapping("/profile")
public ResponseEntity<User> getProfile(HttpSession session) {
Long userId = (Long) session.getAttribute("userId");
if (userId == null) {
throw new UnauthorizedException("Not authenticated");
}
return ResponseEntity.ok(userService.findById(userId));
}
Token-Based Authentication (JWT)¶
JWT Structure¶
JWT Implementation¶
@Service
public class JwtService {
@Value("${jwt.secret}")
private String secret;
@Value("${jwt.expiration:3600000}") // 1 hour default
private long expiration;
public String generateToken(User user) {
Map<String, Object> claims = new HashMap<>();
claims.put("roles", user.getRoles());
return Jwts.builder()
.setClaims(claims)
.setSubject(user.getId().toString())
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + expiration))
.signWith(SignatureAlgorithm.HS256, secret)
.compact();
}
public Claims validateToken(String token) {
return Jwts.parser()
.setSigningKey(secret)
.parseClaimsJws(token)
.getBody();
}
public String extractUserId(String token) {
return validateToken(token).getSubject();
}
}
// JWT Filter
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
@Autowired
private JwtService jwtService;
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain chain) throws ServletException, IOException {
String header = request.getHeader("Authorization");
if (header != null && header.startsWith("Bearer ")) {
String token = header.substring(7);
try {
Claims claims = jwtService.validateToken(token);
List<String> roles = claims.get("roles", List.class);
List<GrantedAuthority> authorities = roles.stream()
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toList());
UsernamePasswordAuthenticationToken auth =
new UsernamePasswordAuthenticationToken(
claims.getSubject(), null, authorities);
SecurityContextHolder.getContext().setAuthentication(auth);
} catch (JwtException e) {
// Invalid token - don't set authentication
}
}
chain.doFilter(request, response);
}
}
Access Token + Refresh Token Pattern¶
@Service
public class TokenService {
private static final long ACCESS_TOKEN_VALIDITY = 15 * 60 * 1000; // 15 min
private static final long REFRESH_TOKEN_VALIDITY = 7 * 24 * 60 * 60 * 1000; // 7 days
@Autowired
private RefreshTokenRepository refreshTokenRepository;
public TokenPair createTokenPair(User user) {
String accessToken = generateAccessToken(user);
String refreshToken = generateRefreshToken(user);
// Store refresh token in database (for revocation)
RefreshToken rt = new RefreshToken();
rt.setToken(refreshToken);
rt.setUserId(user.getId());
rt.setExpiryDate(Instant.now().plusMillis(REFRESH_TOKEN_VALIDITY));
refreshTokenRepository.save(rt);
return new TokenPair(accessToken, refreshToken);
}
public TokenPair refreshAccessToken(String refreshToken) {
RefreshToken stored = refreshTokenRepository.findByToken(refreshToken)
.orElseThrow(() -> new InvalidTokenException("Invalid refresh token"));
if (stored.getExpiryDate().isBefore(Instant.now())) {
refreshTokenRepository.delete(stored);
throw new InvalidTokenException("Refresh token expired");
}
User user = userRepository.findById(stored.getUserId()).orElseThrow();
// Rotate refresh token (optional but recommended)
refreshTokenRepository.delete(stored);
return createTokenPair(user);
}
public void revokeRefreshToken(String refreshToken) {
refreshTokenRepository.deleteByToken(refreshToken);
}
public void revokeAllUserTokens(Long userId) {
refreshTokenRepository.deleteByUserId(userId);
}
}
Authorization Models¶
Role-Based Access Control (RBAC)¶
// Spring Security RBAC
@PreAuthorize("hasRole('ADMIN')")
@DeleteMapping("/users/{id}")
public void deleteUser(@PathVariable Long id) {
userService.delete(id);
}
@PreAuthorize("hasAnyRole('ADMIN', 'MANAGER')")
@GetMapping("/reports")
public List<Report> getReports() {
return reportService.findAll();
}
// Permission-based
@PreAuthorize("hasAuthority('user:delete')")
@DeleteMapping("/users/{id}")
public void deleteUser(@PathVariable Long id) {
userService.delete(id);
}
Attribute-Based Access Control (ABAC)¶
// ABAC-style authorization
@PreAuthorize("@documentAuthz.canEdit(#documentId, authentication)")
@PutMapping("/documents/{documentId}")
public Document updateDocument(@PathVariable Long documentId,
@RequestBody Document document) {
return documentService.update(documentId, document);
}
@Service("documentAuthz")
public class DocumentAuthorization {
public boolean canEdit(Long documentId, Authentication auth) {
Document doc = documentRepository.findById(documentId).orElse(null);
if (doc == null) return false;
User user = (User) auth.getPrincipal();
// Check multiple attributes
return doc.getOwnerId().equals(user.getId()) ||
(doc.getDepartment().equals(user.getDepartment()) &&
doc.getStatus().equals("DRAFT")) ||
user.getRoles().contains("ADMIN");
}
}
Relationship-Based Access Control (ReBAC)¶
API Key Authentication¶
@Service
public class ApiKeyService {
public ApiKey createApiKey(Long userId, String name, Set<String> scopes) {
String rawKey = generateSecureKey();
String hashedKey = hashKey(rawKey);
ApiKey apiKey = new ApiKey();
apiKey.setKeyHash(hashedKey);
apiKey.setKeyPrefix(rawKey.substring(0, 8)); // For identification
apiKey.setUserId(userId);
apiKey.setName(name);
apiKey.setScopes(scopes);
apiKey.setCreatedAt(Instant.now());
apiKeyRepository.save(apiKey);
// Return raw key only once - user must save it
apiKey.setRawKey(rawKey);
return apiKey;
}
public Optional<ApiKey> validateKey(String rawKey) {
String hashedKey = hashKey(rawKey);
return apiKeyRepository.findByKeyHash(hashedKey)
.filter(key -> !key.isRevoked())
.filter(key -> key.getExpiresAt() == null ||
key.getExpiresAt().isAfter(Instant.now()));
}
private String generateSecureKey() {
byte[] bytes = new byte[32];
new SecureRandom().nextBytes(bytes);
return Base64.getUrlEncoder().withoutPadding().encodeToString(bytes);
}
private String hashKey(String rawKey) {
return DigestUtils.sha256Hex(rawKey);
}
}
Single Sign-On (SSO)¶
Multi-Factor Authentication (MFA)¶
TOTP Implementation¶
// Using aerogear-otp-java or similar library
@Service
public class TotpService {
private static final int SECRET_SIZE = 20;
public String generateSecret() {
byte[] buffer = new byte[SECRET_SIZE];
new SecureRandom().nextBytes(buffer);
return new Base32().encodeToString(buffer);
}
public String getQrCodeUri(String secret, String email, String issuer) {
return String.format(
"otpauth://totp/%s:%s?secret=%s&issuer=%s&algorithm=SHA1&digits=6&period=30",
URLEncoder.encode(issuer, StandardCharsets.UTF_8),
URLEncoder.encode(email, StandardCharsets.UTF_8),
secret,
URLEncoder.encode(issuer, StandardCharsets.UTF_8)
);
}
public boolean verifyCode(String secret, String code) {
Totp totp = new Totp(secret);
return totp.verify(code);
}
}
// MFA-enabled login flow
@PostMapping("/login")
public ResponseEntity<?> login(@RequestBody LoginRequest request) {
User user = authService.validateCredentials(request);
if (user.isMfaEnabled()) {
// Return partial token, require MFA verification
String partialToken = jwtService.generatePartialToken(user);
return ResponseEntity.ok(new MfaRequiredResponse(partialToken));
}
return ResponseEntity.ok(jwtService.generateFullToken(user));
}
@PostMapping("/login/mfa")
public ResponseEntity<?> verifyMfa(@RequestBody MfaRequest request) {
Claims claims = jwtService.validatePartialToken(request.getPartialToken());
User user = userService.findById(claims.getSubject());
if (!totpService.verifyCode(user.getMfaSecret(), request.getCode())) {
throw new InvalidMfaCodeException("Invalid MFA code");
}
return ResponseEntity.ok(jwtService.generateFullToken(user));
}
Security Best Practices¶
Common Interview Questions¶
- Session vs JWT?
- Session: Stateful, server stores data, easy to invalidate
-
JWT: Stateless, self-contained, scalable, hard to revoke
-
How to handle token revocation?
- Short-lived access tokens + refresh tokens
- Blacklist for immediate revocation (Redis)
-
Token versioning per user
-
RBAC vs ABAC?
- RBAC: Simple, role-based, coarse-grained
-
ABAC: Complex, attribute-based, fine-grained
-
How to secure password storage?
- Use bcrypt/Argon2 with appropriate work factor
- Never store plain text or weak hashes
-
Add pepper (application-level secret)
-
What is OAuth 2.0 vs OIDC?
- OAuth 2.0: Authorization framework (access tokens)
- OIDC: Authentication layer on OAuth 2.0 (ID tokens)
- *