API Authentication & Authorization
Authorization Code with PKCE (`code_verifier` & `code_challenge`). Prevents authorization code interception on single-page & mobile apps.
OPTIONS /api/v2/orders HTTP/1.1
Host: api.example.com
Origin: https://app.example.com
Access-Control-Request-Method: DELETE
Access-Control-Request-Headers: Authorization, Content-Type
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Methods: GET, POST, DELETE, OPTIONS
Access-Control-Allow-Headers: Authorization, Content-Type
Access-Control-Max-Age: 86400Authentication vs Authorization
| Concept | Question | Example |
|---|---|---|
| Authentication (AuthN) | Who are you? | Verifying a JWT signature proves the token is from a trusted issuer |
| Authorization (AuthZ) | What can you do? | Checking if the user has the ROLE_ADMIN scope to delete resources |
API Key Authentication
The simplest mechanism β a secret token passed with each request.
For detailed patterns on managing long-lived refresh tokens, refresh token rotation (RTR), stolen token reuse detection, emergency account compromise response, and multi-device session invalidation during password updates, see Refresh Token Security & Multi-Device Session Invalidation.
PKCE (Proof Key for Code Exchange)β
Required for public clients (SPAs, mobile apps) that can't keep a client secret:
// Spring Boot: client credentials with WebClient
@Configuration
public class OAuth2ClientConfig {
@Bean
public WebClient serviceClient(
ReactiveOAuth2AuthorizedClientManager manager) {
ServerOAuth2AuthorizedClientExchangeFilterFunction filter =
new ServerOAuth2AuthorizedClientExchangeFilterFunction(manager);
filter.setDefaultClientRegistrationId("service-b");
return WebClient.builder()
.filter(filter) // auto-attaches Bearer token
.baseUrl("https://service-b.internal")
.build();
}
}
# application.yml
spring:
security:
oauth2:
client:
registration:
service-b:
provider: keycloak
client-id: service-a
client-secret: ${CLIENT_SECRET}
authorization-grant-type: client_credentials
scope: read:orders
provider:
keycloak:
token-uri: https://auth.example.com/realms/myapp/protocol/openid-connect/token
Device Code Flowβ
For devices with limited input (smart TVs, IoT):
Device β /device_authorization β gets device_code + user_code + verification_uri
Device shows: "Go to example.com/activate and enter: XKCD-1234"
Device polls /token with device_code until user completes login on another device
Refresh Token Flow
// Access tokens are short-lived (1h); refresh tokens are long-lived (days/weeks)
// Spring handles refresh automatically when using OAuth2 client
POST /token
grant_type=refresh_token
refresh_token=<refresh_token>
client_id=<client_id>
// Response: new access_token (+ optionally new refresh_token)
OIDC β OpenID Connect
An identity layer on top of OAuth 2.0 that adds:
- ID Token: JWT containing user identity (sub, email, name)
- UserInfo endpoint: fetch additional user claims
- Standard scopes:
openid,profile,email,address,phone
// Spring Boot OIDC login (Full SSO setup)
# application.yml
spring:
security:
oauth2:
client:
registration:
google:
client-id: ${GOOGLE_CLIENT_ID}
client-secret: ${GOOGLE_CLIENT_SECRET}
scope: openid,profile,email
provider:
google:
authorization-uri: https://accounts.google.com/o/oauth2/v2/auth
token-uri: https://oauth2.googleapis.com/token
jwk-set-uri: https://www.googleapis.com/oauth2/v3/certs
user-info-uri: https://www.googleapis.com/oauth2/v3/userinfo
Token Revocation & Introspection
The JWT Revocation Problem
JWTs are stateless β once issued, the server can't invalidate them before expiry. Solutions:
| Strategy | Description | Trade-off |
|---|---|---|
| Short expiry | 5β15 min access tokens | Frequent refresh needed |
| Blacklist | Store revoked jti in Redis | Adds latency; stateful |
| Token introspection | Ask auth server if token is still valid | Extra network call |
| Rotating refresh tokens | Old refresh token invalidated on use | Detects token theft |
// Redis-based token blacklist
@Service
public class TokenBlacklistService {
@Autowired RedisTemplate<String, String> redis;
public void revoke(String jti, long expiresInSeconds) {
redis.opsForValue().set("revoked:" + jti, "true",
Duration.ofSeconds(expiresInSeconds));
}
public boolean isRevoked(String jti) {
return Boolean.TRUE.equals(redis.hasKey("revoked:" + jti));
}
}
// Custom JWT decoder that checks blacklist
@Bean
public JwtDecoder jwtDecoder() {
NimbusJwtDecoder decoder = NimbusJwtDecoder
.withJwkSetUri("https://auth.example.com/.well-known/jwks.json")
.build();
decoder.setJwtValidator(new DelegatingOAuth2TokenValidator<>(
JwtValidators.createDefault(),
token -> {
String jti = token.getId();
if (blacklist.isRevoked(jti))
return OAuth2TokenValidatorResult.failure(
new OAuth2Error("revoked_token"));
return OAuth2TokenValidatorResult.success();
}
));
return decoder;
}
mTLS β Mutual TLS
Both client and server present certificates. Used for service-to-service (zero trust).
Regular TLS: Client verifies server's certificate
mTLS: Server also verifies client's certificate β bidirectional trust
// Spring Boot: configure mTLS on server
# application.yml
server:
ssl:
key-store: classpath:server-keystore.p12
key-store-password: ${KEYSTORE_PASSWORD}
key-store-type: PKCS12
trust-store: classpath:client-truststore.p12
trust-store-password: ${TRUSTSTORE_PASSWORD}
client-auth: need # require client cert
// Spring Boot: WebClient with client certificate
@Bean
public WebClient mtlsWebClient() throws Exception {
SslContext sslContext = SslContextBuilder.forClient()
.keyManager(clientCertFile, clientKeyFile)
.trustManager(caCertFile)
.build();
HttpClient httpClient = HttpClient.create()
.secure(spec -> spec.sslContext(sslContext));
return WebClient.builder()
.clientConnector(new ReactorClientHttpConnector(httpClient))
.build();
}
Security Headers
// Spring Security: add security headers
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.headers(headers -> headers
.contentSecurityPolicy(csp ->
csp.policyDirectives("default-src 'self'; script-src 'self'"))
.frameOptions(frame -> frame.deny())
.httpStrictTransportSecurity(hsts -> hsts
.includeSubDomains(true)
.maxAgeInSeconds(31536000)) // 1 year
.xssProtection(xss -> xss.disable()) // use CSP instead
.referrerPolicy(ref ->
ref.policy(ReferrerPolicyHeaderWriter.ReferrerPolicy.STRICT_ORIGIN))
);
return http.build();
}
Rate Limiting
// Bucket4j: token bucket rate limiter
@Component
public class RateLimitFilter extends OncePerRequestFilter {
private final Cache<String, Bucket> buckets = Caffeine.newBuilder()
.expireAfterAccess(1, TimeUnit.HOURS)
.build();
@Override
protected void doFilterInternal(HttpServletRequest req,
HttpServletResponse res, FilterChain chain) throws IOException, ServletException {
String clientId = extractClientId(req); // API key or IP
Bucket bucket = buckets.get(clientId, k ->
Bucket.builder()
.addLimit(Bandwidth.classic(100, Refill.greedy(100, Duration.ofMinutes(1))))
.build());
if (bucket.tryConsume(1)) {
chain.doFilter(req, res);
} else {
res.setStatus(429); // Too Many Requests
res.setHeader("Retry-After", "60");
res.setHeader("X-RateLimit-Limit", "100");
res.setHeader("X-RateLimit-Remaining", "0");
res.getWriter().write("{\"error\":\"rate_limit_exceeded\"}");
}
}
}
Interview Questions
Q1. What is the difference between OAuth 2.0 and OIDC?
OAuth 2.0 is an authorization framework β it grants third-party apps access to resources on behalf of a user (access tokens, scopes). It doesn't define user identity. OIDC (OpenID Connect) is an identity layer built on top of OAuth 2.0 β it adds an ID Token (a JWT with user identity claims like
sub,
Q2. What are the JWT claims iss, sub, aud, exp, iat?
iss(Issuer): who created and signed the token (auth server URL).sub(Subject): who the token is about (user ID).aud(Audience): intended recipient(s) β validate that your API is in this list.exp(Expiration): Unix timestamp after which the token is invalid.iat(Issued At): when the token was created. Always validateexpandaudin addition to the signature.
Q3. Why is RS256 preferred over HS256 for JWTs in microservices?
HS256 uses a shared secret β every service that validates tokens must know the same secret, which becomes a security risk as you scale. RS256 uses asymmetric RSA keys: the auth server signs with its private key; all services validate with the public key. Compromising a resource server doesn't expose the signing key. The public key can be distributed via JWKS endpoint (
/.well-known/jwks.json).
Q4. How do you securely handle token revocation with JWTs?
JWTs are stateless, so traditional revocation requires: (1) Short expiry (5β15 min) + refresh tokens. (2) A revocation blacklist in Redis keyed by
jticlaim β check on every request (adds latency). (3) Token introspection β ask the auth server on each request (most accurate, most overhead). (4) Rotating refresh tokens β detect theft when old refresh token is reused. Strategy depends on security requirements vs latency tolerance.
Q5. What is PKCE and why is it required for public clients?
PKCE (Proof Key for Code Exchange) prevents authorization code interception attacks for public clients (SPAs, mobile apps) that can't securely store a client secret. The client generates a random
code_verifier, computescode_challenge = SHA256(code_verifier), includes the challenge in the auth request, and the verifier in the token request. The auth server verifies they match β an attacker who intercepts the auth code can't exchange it without the verifier.
Q6. What is the Client Credentials flow and when is it used?
Client Credentials is used for machine-to-machine (M2M) communication with no user involvement. Service A authenticates itself with
client_id+client_secretdirectly to the auth server, receives an access token, and uses it to call Service B. Used for internal microservice-to-service calls, batch jobs, background workers. Spring Boot auto-handles token refresh with the OAuth2 client configured withauthorization-grant-type: client_credentials.
Q7. What is mTLS and how does it differ from regular TLS?
Regular TLS: only the client verifies the server's certificate (one-way authentication). mTLS (Mutual TLS): both sides present and verify certificates β the server also verifies the client's cert. This provides cryptographic proof of identity for both parties, making it ideal for zero-trust service-to-service communication. It's the strongest form of service authentication; in service meshes like Istio, mTLS is often applied automatically via sidecars.
Q8. What is token introspection and when would you use it over local JWT validation?
Token introspection (RFC 7662) involves calling the auth server's
/introspectendpoint on every request to check if a token is still valid and active. Unlike local validation (check signature + expiry), introspection can detect revoked tokens immediately. Trade-off: adds a network call to every request (~5β20ms). Use local JWT validation with short expiry for most cases; use introspection for high-security scenarios (banking, healthcare) where immediate revocation is critical.
