Skip to main content

Security Patterns


Authentication vs Authorizationโ€‹

Authentication (AuthN)Authorization (AuthZ)
QuestionWho are you?What can you do?
MechanismJWT, sessions, API keysRBAC, ABAC, ACL
Failure code401 Unauthorized403 Forbidden

JWT (JSON Web Token)โ€‹

Header.Payload.Signature
eyJhbGciOiJSUzI1NiJ9.eyJzdWIiOiIxMjMifQ.signature

Structureโ€‹

// Header
{ "alg": "RS256", "typ": "JWT" }

// Payload (claims)
{
"sub": "user-123",
"email": "[email protected]",
"roles": ["ROLE_USER", "ROLE_ADMIN"],
"iat": 1700000000,
"exp": 1700003600
}

Spring Security JWTโ€‹

@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http
.csrf(csrf -> csrf.disable())
.sessionManagement(s -> s.sessionCreationPolicy(STATELESS))
.authorizeHttpRequests(auth -> auth
.requestMatchers("/public/**").permitAll()
.requestMatchers("/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()
)
.oauth2ResourceServer(oauth2 -> oauth2.jwt(Customizer.withDefaults()))
.build();
}

@Bean
public JwtDecoder jwtDecoder() {
return NimbusJwtDecoder.withPublicKey(rsaPublicKey()).build();
}
}

JWT Trade-offsโ€‹

AdvantageDisadvantage
Stateless (no DB lookup per request)Cannot be revoked before expiry
Self-contained claimsToken size larger than session ID
Cross-service verificationMust use short expiry + refresh tokens

Token Revocationโ€‹

// Blocklist with Redis (for logout/revocation)
public void revokeToken(String jti, Instant expiry) {
Duration ttl = Duration.between(Instant.now(), expiry);
redis.opsForValue().set("revoked:" + jti, "1", ttl);
}

// In JWT filter
public boolean isRevoked(String jti) {
return redis.hasKey("revoked:" + jti);
}

OAuth 2.0 / OIDCโ€‹

Authorization Code Flow (Web Apps)โ€‹

1. User clicks "Login with Google"
2. Redirect to Google /authorize?client_id=...&scope=openid email
3. User authenticates with Google
4. Google redirects back: /callback?code=AUTH_CODE
5. Your server exchanges code โ†’ access_token + id_token
6. Your server validates id_token โ†’ extracts user info

Client Credentials Flow (Service-to-Service)โ€‹

Service A โ†’ POST /oauth/token (client_id, client_secret, grant_type=client_credentials)
โ† access_token
Service A โ†’ GET /api/resource (Authorization: Bearer access_token)

RBAC (Role-Based Access Control)โ€‹

User โ†’ Role โ†’ Permission

Alice โ†’ [ADMIN, USER] โ†’ [READ_ALL, WRITE_ALL, DELETE_ALL]
Bob โ†’ [USER] โ†’ [READ_OWN, WRITE_OWN]
// Method-level security
@PreAuthorize("hasRole('ADMIN') or #userId == authentication.principal.id")
public void deleteUser(Long userId) { ... }

@PostAuthorize("returnObject.userId == authentication.principal.id")
public Order getOrder(Long orderId) { ... }

// Custom permission evaluator
@PreAuthorize("hasPermission(#orderId, 'ORDER', 'READ')")
public Order getOrder(Long orderId) { ... }

Secrets Managementโ€‹

Neverโ€‹

  • Hardcode secrets in source code
  • Store secrets in application.properties committed to git
  • Log secrets

Alwaysโ€‹

// Use environment variables
@Value("${DB_PASSWORD}")
private String dbPassword;

// Or Spring Cloud Vault / AWS Secrets Manager
@Bean
public VaultTemplate vaultTemplate() {
// Auto-injects secrets from HashiCorp Vault
}
# Spring Cloud Vault
spring:
cloud:
vault:
uri: https://vault.example.com
authentication: AWS_IAM
aws-iam:
role: my-service-role

Secret Rotationโ€‹

  • Secrets should have TTL (short-lived)
  • Rotate database passwords without downtime (dual-password period)
  • Use dynamic secrets (Vault generates one-time DB credentials per request)

Rate Limiting (Security Aspect)โ€‹

// Per-IP rate limiting for auth endpoints
@Component
public class LoginRateLimitFilter extends OncePerRequestFilter {
private static final String PREFIX = "login_attempts:";

protected void doFilterInternal(HttpServletRequest req, ...) {
if (req.getRequestURI().equals("/api/auth/login")) {
String ip = req.getRemoteAddr();
String key = PREFIX + ip;
Long attempts = redis.opsForValue().increment(key);
if (attempts == 1) redis.expire(key, Duration.ofMinutes(15));

if (attempts > 10) {
response.setStatus(429);
response.getWriter().write("{\"error\": \"Too many login attempts\"}");
return;
}
}
chain.doFilter(req, response);
}
}

Zero Trust Architectureโ€‹

"Never trust, always verify." No implicit trust based on network location.

Principles:

  1. Verify explicitly (every request, every time)
  2. Least privilege access
  3. Assume breach โ€” limit blast radius
Traditional: Inside network = trusted
Zero Trust: Every service call must authenticate, even internal

Implementation:
- mTLS between all services (mutual authentication)
- Service accounts with minimal permissions
- Network policies (only allow necessary traffic)
- No "bastion" or implicit trust for internal IPs

OWASP Top 10 for APIsโ€‹

VulnerabilityExampleFix
Broken Object Level AuthGET /orders/123 (not your order)Always verify ownership
Broken AuthWeak JWT secret, no token expiryRS256, short expiry, rotation
Excessive Data ExposureReturn full user object including password hashUse DTOs, project only needed fields
Rate Limiting MissingBrute force loginRate limit auth endpoints
Broken Function Level AuthRegular user calls /admin endpoint@PreAuthorize on every endpoint
Mass AssignmentPATCH /users/{id} with {"role":"ADMIN"}Whitelist updatable fields
Security MisconfigurationDefault creds, verbose error messagesAudit configs, generic error messages
InjectionSQL injection via string concatenationParameterized queries, JPA
Improper Assets ManagementOld v1 API with no auth still runningAPI versioning, retire old versions
Insufficient LoggingNo audit log for sensitive operationsLog all auth events

SQL Injection Preventionโ€‹

// BAD
String query = "SELECT * FROM users WHERE email = '" + email + "'";
// Attacker: email = "' OR '1'='1"

// GOOD โ€” JPA (parameterized)
@Query("SELECT u FROM User u WHERE u.email = :email")
Optional<User> findByEmail(@Param("email") String email);

// GOOD โ€” JDBC template
jdbcTemplate.queryForObject(
"SELECT * FROM users WHERE email = ?",
userRowMapper, email // Parameterized โ€” safe
);

HTTPS / TLS Best Practicesโ€‹

# Spring Boot โ€” enforce HTTPS
server:
ssl:
enabled: true
key-store: classpath:keystore.p12
key-store-type: PKCS12
http2:
enabled: true

# Redirect HTTP to HTTPS
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.requiresChannel(channel -> channel
.anyRequest().requiresSecure()
);
http.headers(headers -> headers
.httpStrictTransportSecurity(hsts -> hsts
.includeSubDomains(true)
.maxAgeInSeconds(31536000)
)
);
return http.build();
}

Interview Questionsโ€‹

Q: What is the difference between authentication and authorization?โ€‹

A: Authentication verifies who the user/service is; authorization decides what that identity can access. Always authenticate first, then enforce resource-level permissions.

Q: How does JWT work? What are its advantages and disadvantages over session tokens?โ€‹

A: JWT is a signed token containing claims validated without server-side session lookup. It scales well statelessly but revocation and claim bloat are harder than opaque session IDs.

Q: What is the OAuth 2.0 Authorization Code flow?โ€‹

A: User authenticates at the authorization server, client gets an auth code, then exchanges it (with client auth/PKCE) for tokens. It keeps access tokens off the browser redirect channel.

Q: How do you revoke a JWT before it expires?โ€‹

A: Use short-lived access tokens plus refresh-token rotation and server-side denylist/jti checks for high-risk revocations. Token introspection is another option for opaque tokens.

Q: What is Zero Trust architecture?โ€‹

A: Zero Trust assumes no implicit network trust and verifies every request continuously. It enforces strong identity, least privilege, and device/context-aware policy.

Q: How do you prevent SQL injection in a Spring Boot application?โ€‹

A: Use parameterized queries/JPA bindings, never string-concatenate SQL, and validate input at boundaries. Apply least-privilege DB accounts and query allowlists where possible.

Q: What is RBAC and how do you implement it with Spring Security?โ€‹

A: RBAC grants permissions by role rather than per-user hardcoding. In Spring Security, map roles/authorities from identity provider claims and enforce with method or URL policies.

Q: How should secrets (API keys, DB passwords) be managed in a microservices system?โ€‹

A: Store secrets in a dedicated manager (Vault/AWS Secrets Manager), inject at runtime, and rotate automatically. Never commit secrets to code or logs.

Q: What is OWASP Broken Object Level Authorization? Give an example.โ€‹

A: BOLA occurs when APIs expose object IDs but do not enforce ownership checks. Example: changing /orders/123 to /orders/124 returns another user's order.

Q: How do you rate limit authentication endpoints to prevent brute force?โ€‹

A: Apply per-IP and per-account limits with exponential backoff and temporary lockouts. Combine with bot detection, MFA prompts, and suspicious activity monitoring.


JWT vs. Opaque Tokens: Senior Deep Diveโ€‹

Chapter 11 Reference

Building Microservices Chapter 11 dedicates substantial coverage to the debate between JWT (stateless) and opaque (server-side) token strategies, noting that neither is universally superior โ€” the right choice depends on operational context.

Comprehensive Comparisonโ€‹

DimensionJWT (Self-Contained)Opaque Token (Reference)
ValidationCryptographic signature check (no network call)Must call token introspection endpoint
RevocationImpossible without denylist; wait for expiryImmediate โ€” delete from store
Size500โ€“2000 bytes (claims bloat)32โ€“64 bytes (random token ID)
Cross-serviceAny service can validate with public keyAll services must call auth server
Claims freshnessStale for lifetime of tokenAlways current (introspection reads live state)
Offline validationYes (no network dependency)No (requires auth server)
Best forInternal service-to-service, stateless APIsUser sessions, admin tokens, high-revocation-needs

Token Lifecycle: JWT Best Practicesโ€‹

Access Token: short-lived (15 min)
Refresh Token: long-lived (7โ€“30 days), rotated on each use

Client โ†’ POST /auth/login โ†’ access_token (15min) + refresh_token (30d)
โ”‚
โ”œโ”€ Use access_token for API calls
โ”‚
โ””โ”€ access_token expires โ†’
POST /auth/refresh { refresh_token }
โ†’ new access_token (15min) + new refresh_token (30d)
โ†’ old refresh_token immediately invalidated
// Refresh Token Rotation with Redis
@Service
public class TokenService {
private final RedisTemplate<String, String> redis;
private final JwtUtil jwtUtil;

// Refresh: atomic rotate
@Transactional
public TokenPair refresh(String oldRefreshToken) {
// 1. Validate old refresh token exists
String userId = redis.opsForValue().get("rt:" + oldRefreshToken);
if (userId == null) {
// Potential replay attack โ€” invalidate ALL tokens for this user
invalidateAllUserTokens(userId);
throw new SecurityException("Invalid or replayed refresh token");
}

// 2. Delete old refresh token atomically (prevent concurrent replay)
Boolean deleted = redis.delete("rt:" + oldRefreshToken);
if (!deleted) throw new SecurityException("Concurrent refresh detected");

// 3. Issue new pair
String newAccessToken = jwtUtil.generateAccess(userId);
String newRefreshToken = UUID.randomUUID().toString();
redis.opsForValue().set("rt:" + newRefreshToken, userId,
Duration.ofDays(30));

return new TokenPair(newAccessToken, newRefreshToken);
}
}

JWT Algorithm Selectionโ€‹

AlgorithmTypeKey SizeSecurity LevelUse Case
HS256Symmetric (HMAC)256-bit shared secretLowโ€“MediumInternal services sharing same secret
HS512Symmetric (HMAC)512-bit shared secretMediumInternal with longer secret
RS256Asymmetric (RSA)2048-bit key pairHighPublic APIs, external clients
ES256Asymmetric (ECDSA)256-bit key pairHighMobile, constrained clients (smaller token)
PS256Asymmetric (RSA-PSS)2048-bit key pairHighestFAPI, Open Banking compliance
โŒ NEVER use HS256 for external-facing APIs:
- All services share the same secret
- Any compromised service can forge tokens for any other service

โœ… RS256/ES256 for external: sign with private key, verify with public key
- Public key can be distributed to all services via JWKS endpoint
- Only the auth server holds the private key

JWKS (JSON Web Key Set) โ€” Public Key Distributionโ€‹

// Auth server exposes public keys via JWKS endpoint
// GET /.well-known/jwks.json
// {
// "keys": [{
// "kty": "RSA",
// "kid": "2024-01",
// "use": "sig",
// "n": "...",
// "e": "AQAB"
// }]
// }

// Resource server auto-fetches and caches JWKS
@Bean
public JwtDecoder jwtDecoder() {
return NimbusJwtDecoder
.withJwkSetUri("https://auth.example.com/.well-known/jwks.json")
.build();
// Spring caches the JWKS and re-fetches on key rotation
}

mTLS (Mutual TLS): Zero-Trust Service Identityโ€‹

How mTLS Worksโ€‹

Standard TLS: client verifies server's certificate.
mTLS: server AND client each verify each other's certificates.

Service A โ”€โ”€โ”€โ”€ mTLS handshake โ”€โ”€โ”€โ”€โ–บ Service B

1. B presents its certificate (signed by cluster CA)
2. A verifies B's cert against trusted CA
3. A presents its certificate
4. B verifies A's cert against trusted CA
5. Encrypted tunnel established with mutual identity

Istio mTLS: Automatic Certificate Managementโ€‹

# PeerAuthentication: enforce mTLS in namespace
apiVersion: security.istio.io/v1beta1
kind: PeerAuthentication
metadata:
name: default
namespace: production
spec:
mtls:
mode: STRICT # STRICT = reject all non-mTLS traffic
# PERMISSIVE = allow both mTLS and plain text (migration mode)
# AuthorizationPolicy: fine-grained service-to-service access control
apiVersion: security.istio.io/v1beta1
kind: AuthorizationPolicy
metadata:
name: payment-access
namespace: production
spec:
selector:
matchLabels:
app: payment-service
rules:
- from:
- source:
# Only checkout-service and order-service can call payment-service
principals:
- cluster.local/ns/production/sa/checkout-service
- cluster.local/ns/production/sa/order-service
to:
- operation:
methods: ["POST"]
paths: ["/api/payments/*"]

mTLS vs. JWT for Internal Service Authโ€‹

ScenariomTLSJWT (service credentials)
Service identityCryptographic certificateClient credentials token
RotationAutomatic (Istio rotates every 24h)Manual or scheduled
GranularityService-level identityCan embed scopes/roles
Setup complexityHigh (service mesh required)Medium
East-west trafficIdealOverhead per-request
North-south (external)Not applicableStandard

RBAC vs. ABAC: Authorization Model Deep Diveโ€‹

RBAC (Role-Based Access Control)โ€‹

User โ”€โ”€โ†’ Role โ”€โ”€โ†’ Permission

Pros: Simple to understand and audit
Cons: Role explosion (hundreds of roles), coarse-grained, can't express context
// Spring Security RBAC
@PreAuthorize("hasRole('ORDER_MANAGER') or hasRole('ADMIN')")
public void cancelOrder(Long orderId) { ... }

// Problem: What if ORDER_MANAGER can only cancel THEIR OWN team's orders?
// RBAC can't express this without creating per-team roles (role explosion)

ABAC (Attribute-Based Access Control)โ€‹

Subject (user.department=Finance) + Action (UPDATE) + Resource (invoice.department=Finance)
+ Environment (time.hour=business_hours)
โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
Policy Decision: ALLOW
// Spring Security ABAC with SpEL
@PreAuthorize("@securityService.canModifyOrder(authentication, #orderId)")
public void cancelOrder(Long orderId) { ... }

@Service
public class SecurityService {
public boolean canModifyOrder(Authentication auth, Long orderId) {
Order order = orderRepository.findById(orderId).orElseThrow();
UserDetails user = (UserDetails) auth.getPrincipal();

// Attribute-based: user must own the order OR be in same department
return order.getOwnerId().equals(user.getId())
|| order.getDepartment().equals(user.getDepartment())
|| user.getAuthorities().contains(new SimpleGrantedAuthority("ROLE_ADMIN"));
}
}

ReBAC (Relationship-Based Access Control)โ€‹

Google Zanzibar model: permissions defined by relationships between objects.

tuple: document:readme#owner@user:alice
tuple: document:readme#viewer@group:engineering
tuple: group:engineering#member@user:bob

Query: Can user:bob view document:readme?
bob โ”€memberโ”€โ–บ engineering โ”€viewerโ”€โ–บ readme โœ“ YES

Used by: Google Drive, GitHub, Notion, Carta.

Authorization Model Selection Guideโ€‹

FactorRBACABACReBAC
Number of roles< 50 manageableAny numberAny number
Context-aware decisionsNoYesNo
Relationship-based accessNoPartialYes
Audit trail simplicitySimpleComplexMedium
PerformanceFast (DB lookup)Variable (policy evaluation)Can be expensive (graph traversal)
ExamplesSimple SaaS, internal toolsHealthcare, finance, governmentGoogle Drive, GitHub, Linear

Zero Trust Network Architectureโ€‹

The Perimeter Security Fallacyโ€‹

Old model (Castle & Moat):
External users โ”€โ”€โ”€โ”€ firewall โ”€โ”€โ”€โ”€โ–บ internal network
"Anything inside is trusted"
Problem: Lateral movement after breach, insider threats, cloud makes perimeter meaningless

Zero Trust:
Every request verified regardless of source IP
"Never trust, always verify, least privilege"

Zero Trust Pillars (NIST SP 800-207)โ€‹

PillarImplementation
IdentityStrong MFA + continuous validation (not just at login)
DeviceCertificate-based device auth, endpoint posture checks
NetworkmTLS east-west, network segmentation, deny-by-default
ApplicationPer-request authorization, ABAC policies
DataEncryption at rest + in transit, data classification

Kubernetes Network Policy: Zero Trust Baselineโ€‹

# Default deny all ingress and egress in production namespace
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny-all
namespace: production
spec:
podSelector: {} # Select all pods
policyTypes:
- Ingress
- Egress
---
# Allow: order-service can reach payment-service on port 8080 only
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-order-to-payment
namespace: production
spec:
podSelector:
matchLabels:
app: payment-service
ingress:
- from:
- podSelector:
matchLabels:
app: order-service
ports:
- protocol: TCP
port: 8080
---
# Allow: all services can reach DNS (kube-dns)
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-dns-egress
namespace: production
spec:
podSelector: {}
egress:
- ports:
- protocol: UDP
port: 53

OAuth 2.0: Advanced Flowsโ€‹

PKCE (Proof Key for Code Exchange)โ€‹

Prevents authorization code interception attacks in public clients (SPA, mobile apps).

Traditional auth code flow weakness:
Malicious app on same device intercepts the /callback?code=AUTH_CODE redirect

PKCE solution:
1. Client generates: code_verifier = random(128 chars)
2. Client computes: code_challenge = base64url(SHA256(code_verifier))
3. /authorize?code_challenge=..&code_challenge_method=S256
4. /token: client sends code_verifier โ€” auth server verifies SHA256 matches
โ†’ Intercepted code is useless without code_verifier

Client Credentials Flow: Service-to-Serviceโ€‹

// Service A fetches access token to call Service B
@Service
public class OAuth2TokenFetcher {
private final OAuth2AuthorizedClientManager clientManager;

public String getTokenForService(String serviceName) {
OAuth2AuthorizeRequest request = OAuth2AuthorizeRequest
.withClientRegistrationId(serviceName)
.principal("service-a")
.build();

OAuth2AuthorizedClient client = clientManager.authorize(request);
return client.getAccessToken().getTokenValue();
}
}
spring:
security:
oauth2:
client:
registration:
payment-service:
client-id: order-service-client
client-secret: ${PAYMENT_SERVICE_CLIENT_SECRET}
authorization-grant-type: client_credentials
scope: payments:write
provider:
payment-service:
token-uri: https://auth.example.com/oauth/token

Token Introspection: Opaque Tokensโ€‹

// Resource server validates opaque token by calling auth server
@Bean
public OpaqueTokenIntrospector introspector() {
return new NimbusOpaqueTokenIntrospector(
"https://auth.example.com/oauth/introspect",
"resource-server-client",
"resource-server-secret"
);
}
// Spring auto-calls introspection endpoint on every request
// Response includes: active, scope, sub, exp, client_id

Secrets Management: Production Patternsโ€‹

HashiCorp Vault: Dynamic Secretsโ€‹

Dynamic secrets are generated on-demand with a TTL, eliminating long-lived credentials.

Traditional: DB_PASSWORD=static-password stored in .env (rotated manually every 90 days)

Dynamic secrets:
Service A requests DB credentials โ†’ Vault creates: DB_USER=v-app-xyz, DB_PASS=generated-abc (TTL: 1h)
Service A uses credentials for 1 hour
Vault revokes: DROP USER v-app-xyz (automatic)
Next request โ†’ new unique credentials

Benefits:
- No long-lived passwords to steal
- Automatic rotation
- Per-service audit trail
- Revocation without credential rotation ceremony
// Spring Cloud Vault: dynamic DB credentials
@Configuration
public class VaultDatabaseConfig {
@Bean
@ConfigurationProperties("spring.datasource")
public DataSourceProperties dataSourceProperties() {
return new DataSourceProperties();
}
}
spring:
cloud:
vault:
uri: https://vault.example.com:8200
authentication: KUBERNETES # Use K8s service account token
kubernetes:
role: order-service-role # Vault role bound to K8s SA
database:
enabled: true
role: order-service-db-role # Vault DB role with limited permissions
backend: database

Secret Rotation Patternsโ€‹

PatternWhen to UseZero Downtime?
Dual-version secretsDB passwords โ€” add new user, drain to new, delete oldYes
Rolling restartConfig-file secrets โ€” update secret, restart pods rollingYes
Atomic swapRedis/cache credentialsNear-zero
Certificate rotationTLS certs โ€” overlap old+new before removing oldYes
Emergency revocationCompromised secret โ€” immediate effect, brief downtime acceptableNo

API Security: Defense in Depthโ€‹

Input Validation: Multiple Layersโ€‹

// Layer 1: Type constraints (OpenAPI spec auto-validates via Spring)
@RestController
public class UserController {

// Layer 2: Bean Validation on DTO
@PostMapping("/users")
public ResponseEntity<UserResponse> createUser(
@Valid @RequestBody CreateUserRequest req) { ... }
}

@Data
public class CreateUserRequest {
@NotBlank @Size(max = 100)
@Pattern(regexp = "^[a-zA-Z0-9._+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$")
private String email;

@NotBlank @Size(min = 8, max = 100)
private String password;

@Size(max = 50)
@Pattern(regexp = "^[\\p{L}\\s'-]+$") // Unicode letters, spaces, hyphens
private String name;
}

// Layer 3: Domain-level validation
@Service
public class UserService {
public User createUser(CreateUserRequest req) {
if (userRepository.existsByEmail(req.getEmail())) {
throw new ConflictException("Email already registered");
}
// Domain invariants enforced here
}
}

CORS Configurationโ€‹

@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/api/**")
.allowedOrigins(
"https://app.example.com",
"https://admin.example.com"
)
// โŒ Never allowedOrigins("*") for authenticated APIs
.allowedMethods("GET", "POST", "PUT", "DELETE", "PATCH")
.allowedHeaders("Authorization", "Content-Type", "X-Correlation-ID")
.allowCredentials(true) // Required for cookies/auth headers
.maxAge(3600); // Cache preflight for 1 hour
}
}

Security Headersโ€‹

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.headers(headers -> headers
// Prevent clickjacking
.frameOptions(frame -> frame.deny())
// Prevent MIME sniffing
.contentTypeOptions(Customizer.withDefaults())
// XSS protection (for non-API HTML responses)
.xssProtection(Customizer.withDefaults())
// HSTS: force HTTPS for 1 year, including subdomains
.httpStrictTransportSecurity(hsts -> hsts
.includeSubDomains(true)
.maxAgeInSeconds(31536000)
.preload(true)
)
// Content Security Policy
.contentSecurityPolicy(csp -> csp
.policyDirectives(
"default-src 'self'; " +
"script-src 'self'; " +
"style-src 'self' 'unsafe-inline'; " +
"img-src 'self' data: https:; " +
"connect-src 'self' https://api.example.com"
)
)
);
return http.build();
}

Audit Logging: Compliance and Securityโ€‹

What Must Be Auditedโ€‹

// AuditEvent: record sensitive operations with full context
@Service
@Slf4j
public class AuditService {
private final AuditRepository auditRepository;

public void audit(AuditEvent event) {
AuditRecord record = AuditRecord.builder()
.timestamp(Instant.now())
.userId(SecurityContextHolder.getContext()
.getAuthentication().getName())
.action(event.getAction()) // CREATE_USER, DELETE_ORDER, etc.
.resourceType(event.getResourceType())
.resourceId(event.getResourceId())
.result(event.getResult()) // SUCCESS | FAILURE
.ip(event.getIp())
.userAgent(event.getUserAgent())
.changes(event.getChanges()) // Before/after state for mutations
.build();

// Write to append-only audit table (no UPDATE/DELETE permissions)
auditRepository.save(record);

// Also emit structured log for SIEM ingestion
log.info("AUDIT event={} userId={} resource={}/{} result={}",
event.getAction(), record.getUserId(),
event.getResourceType(), event.getResourceId(),
event.getResult());
}
}

Events That Require Audit Loggingโ€‹

CategoryEvents
AuthenticationLogin success/failure, logout, MFA challenges
AuthorizationAccess denied events (403s), permission changes
Data mutationsCreate/update/delete of sensitive resources
Admin actionsRole assignment, secret rotation, config changes
Data exportsBulk data downloads, report generation
Security eventsToken revocation, suspicious activity flags

Security Anti-Patternsโ€‹

Anti-PatternRisk LevelFix
Long-lived tokens (>24h access tokens)High15-min access + refresh rotation
User ID in request bodyCriticalExtract identity from token only
HS256 with weak secretCriticalRS256/ES256 with proper key size
No token expiryCriticalAlways set exp claim
Logging JWT tokensCriticalNever log Authorization header values
RBAC without BOLA checkHighAlways verify resource ownership
Missing rate limit on authHighExponential backoff + lockout
Plain HTTP internal trafficMedium-HighmTLS or at minimum TLS
Secrets in environment variablesMediumVault/Secrets Manager with rotation
Storing raw passwordsCriticalbcrypt/Argon2 with proper cost factor

Interview Questions: Senior Levelโ€‹

Q: When would you choose opaque tokens over JWTs for a microservices system?โ€‹

A: When immediate revocation is required โ€” for example, a user logs out, an account is suspended, or a security breach is detected. JWTs cannot be revoked before expiry without a denylist that requires a network call, effectively eliminating their statelessness advantage. Opaque tokens enable instant revocation by deleting from the token store. The trade-off is that every service must call the introspection endpoint on each request, adding latency and a dependency on the auth server. A hybrid approach works well: short-lived JWTs (5โ€“15 minutes) for normal traffic, with a Redis-backed denylist for high-priority revocations.

Q: Explain the PKCE flow and why it is necessary for single-page applications.โ€‹

A: SPA/mobile apps are public clients โ€” they cannot safely store a client secret. The authorization code flow without PKCE is vulnerable to code interception: a malicious app on the same device could intercept the redirect URI containing the auth code and exchange it for tokens. PKCE adds a cryptographic proof: the client generates a random code_verifier, hashes it to code_challenge, sends the hash in the authorization request, and proves knowledge of the verifier at the token endpoint. An intercepted code is useless without the original verifier, which never leaves the legitimate client.

Q: How does Istio implement mTLS without changing application code?โ€‹

A: Istio injects an Envoy sidecar proxy into every pod. The sidecar intercepts all network traffic before it reaches the application container. For outbound connections, Envoy upgrades plain HTTP to mTLS using a workload certificate issued by Istiod (the control plane). For inbound connections, Envoy terminates the mTLS handshake and forwards plain HTTP to the application on localhost. The application speaks only plain HTTP โ€” the mTLS is entirely managed by the sidecar mesh. Istiod also automatically rotates workload certificates every 24 hours using SPIFFE-compliant SVID certificates.

Q: How do you design an authorization system for a multi-tenant SaaS where users belong to organizations?โ€‹

A: This is a classic relationship-based access control (ReBAC) problem. Model the relationships: user โ†’ member โ†’ organization, organization โ†’ owner โ†’ resource, resource โ†’ permission โ†’ action. Use a policy engine like Zanzibar (or OSS: SpiceDB, Keto) to evaluate permission checks as graph traversals. This naturally handles: users inheriting org-level permissions, resource sharing between orgs, and inheritance hierarchies. Avoid encoding org membership in JWT claims as they become stale โ€” instead check relationships at request time against the policy engine.

Q: How would you implement defense-in-depth for a financial API?โ€‹

A: Layer defenses: (1) Network โ€” mTLS internal traffic, WAF at edge blocking OWASP Top 10. (2) Identity โ€” short-lived RS256 JWTs (5-min access tokens) with refresh rotation and device binding. (3) Authorization โ€” ABAC with per-request ownership checks, not just role checks. (4) Input โ€” JSON schema validation, parameterized queries, content-length limits. (5) Rate limiting โ€” per-IP, per-user, per-API-key token bucket. (6) Audit โ€” every mutation logged with before/after state, user identity, IP, and user agent. (7) Anomaly detection โ€” ML-based fraud scoring on transaction patterns. Each layer must fail independently without causing a complete outage.