JWT & Application Security — Interview Questions

Stack context: This system uses JWT signed by user-service (HS256 via jjwt 0.12.5), stored as a Redis session (session:{jwt}, TTL 1800s). The api-gateway validates sessions by looking up the JWT in Redis. Downstream services receive X-User-Id, X-User-Email, X-User-Role headers injected by the gateway. Spring Security filter chains secure each service.


Q1 — What is a JWT and what are its three parts? junior

Answer: JWT (JSON Web Token) is an open standard (RFC 7519) for securely transmitting claims between parties as a compact, URL-safe string.

Structure: header.payload.signature (three Base64URL-encoded parts, separated by dots)

1. Header:

{
  "alg": "HS256",    // signing algorithm
  "typ": "JWT"       // token type
}

2. Payload (claims):

{
  "sub": "user-123-uuid",          // subject (user ID)
  "email": "user@example.com",     // custom claim
  "role": "USER",                  // custom claim
  "iat": 1700000000,               // issued at (Unix timestamp)
  "exp": 1700003600,               // expiration time (1 hour later)
  "jti": "unique-token-id"         // JWT ID (for revocation)
}

3. Signature:

HMACSHA256(
  base64url(header) + "." + base64url(payload),
  secretKey
)

Verification: The receiver recalculates the signature and compares it to the token's signature. If they match, the payload is authentic and untampered.

Important: JWT payload is Base64URL-encoded, NOT encrypted. Anyone can decode it. Never put sensitive data (passwords, PII beyond necessary) in JWT.


Q2 — What is the difference between HS256, RS256, and ES256 JWT signing algorithms? senior

Answer:

Algorithm Type Key Best for
HS256 Symmetric HMAC Shared secret (same key signs and verifies) Monolith, microservices where all services share the secret
RS256 Asymmetric RSA Private key signs, public key verifies Different services verify without the signing key
ES256 Asymmetric ECDSA Private key signs, public key verifies Same as RS256 but smaller key size, faster

HS256 (this system):

// user-service: sign with shared secret
SecretKey key = Keys.hmacShaKeyFor(jwtSecret.getBytes(StandardCharsets.UTF_8));
String token = Jwts.builder().subject(userId).signWith(key, Jwts.SIG.HS256).compact();

// api-gateway: verify with same shared secret
Claims claims = Jwts.parser().verifyWith(key).build().parseSignedClaims(token).getPayload();

RS256 (better for large systems):

// user-service: sign with PRIVATE key (kept secret)
token = Jwts.builder().signWith(rsaPrivateKey, Jwts.SIG.RS256).compact();

// Any service: verify with PUBLIC key (shared openly — no secret needed)
claims = Jwts.parser().verifyWith(rsaPublicKey).build().parseSignedClaims(token).getPayload();

Security: HS256 requires all services to know the secret — if any service is compromised, the signing capability is exposed. RS256 means only user-service holds the private key; other services only need the public key for verification.


Q3 — What are the standard JWT claims and their meanings? junior

Answer: The JWT spec (RFC 7519) defines registered claims. Custom claims can be added freely.

Registered claims (optional but standard):

Claim Name Meaning
iss Issuer Who issued the token ("user-service")
sub Subject Token subject, typically user ID
aud Audience Intended recipient ("api-gateway")
exp Expiration Time Unix timestamp — token invalid after this
nbf Not Before Unix timestamp — token invalid before this
iat Issued At Unix timestamp of when token was issued
jti JWT ID Unique token identifier (for revocation)

jjwt 0.12.5 example (this system):

String token = Jwts.builder()
    .issuer("user-service")
    .subject(user.getId().toString())
    .claim("email", user.getEmail())
    .claim("role", user.getRole().name())
    .issuedAt(new Date())
    .expiration(new Date(System.currentTimeMillis() + 3_600_000))  // 1 hour
    .id(UUID.randomUUID().toString())  // jti — for revocation tracking
    .signWith(secretKey, Jwts.SIG.HS256)
    .compact();

Q4 — How does this system's JWT session flow work end to end? senior

Answer: This system uses a hybrid approach: JWT + Redis session store. Pure stateless JWT + Redis for revocation.

Login flow:

1. POST /auth/login → user-service
2. user-service validates credentials (BCrypt compare)
3. user-service creates JWT (sub=userId, email, role, exp=+30min)
4. user-service stores session in Redis:
     key:   "session:{jwt_string}"
     value: {userId, email, role}
     TTL:   1800s
5. Returns JWT to client

Request flow:

1. Client → GET /orders → api-gateway (port 8080)
   Authorization: Bearer <jwt>

2. api-gateway's AuthenticationFilter:
   a. Extract JWT from Authorization header
   b. Look up Redis: GET "session:{jwt}"
   c. Redis HIT → extract userId, email, role
   d. Set headers: X-User-Id, X-User-Email, X-User-Role
   e. Forward request to order-service (port 8083)

3. order-service:
   a. No JWT validation needed
   b. Read X-User-Id header from gateway
   c. Process request

4. Logout:
   a. DELETE Redis key "session:{jwt}"
   b. Token is immediately invalid (even if exp hasn't passed)

Why Redis + JWT?


Q5 — How do you handle JWT expiration and token refresh? senior

Answer: When a JWT expires, the user must re-authenticate (or use a refresh token flow).

Access token + Refresh token pattern:

// Login returns both tokens
record LoginResponse(String accessToken, String refreshToken) {}

// Access token: short-lived (15 min – 1 hour)
String accessToken = Jwts.builder()
    .subject(userId)
    .expiration(new Date(System.currentTimeMillis() + 900_000))  // 15 min
    .signWith(key, Jwts.SIG.HS256).compact();

// Refresh token: long-lived (7 days), stored in DB/Redis
String refreshToken = UUID.randomUUID().toString();  // opaque token
redisTemplate.opsForValue().set("refresh:" + refreshToken, userId, Duration.ofDays(7));

Refresh endpoint:

@PostMapping("/auth/refresh")
public ResponseEntity<LoginResponse> refresh(@RequestBody RefreshRequest req) {
    String userId = redisTemplate.opsForValue().get("refresh:" + req.refreshToken());
    if (userId == null) throw new UnauthorizedException("Refresh token expired or invalid");

    String newAccess = generateAccessToken(userId);
    String newRefresh = rotateRefreshToken(req.refreshToken(), userId);  // rotate for security

    return ResponseEntity.ok(new LoginResponse(newAccess, newRefresh));
}

Token rotation: On each refresh, invalidate the old refresh token and issue a new one. This prevents refresh token theft reuse.


Q6 — How does Redis enable JWT revocation? senior

Answer: Pure stateless JWTs cannot be revoked before expiry. Redis solves this by making the token's validity dependent on a Redis lookup.

This system's approach (allowlist pattern):

// Validate token in api-gateway
public Mono<SessionData> validateToken(String jwt) {
    String key = "session:" + jwt;
    return redisTemplate.opsForHash()
        .entries(key)
        .collectMap(entry -> entry.getKey().toString(), entry -> entry.getValue().toString())
        .flatMap(sessionData -> {
            if (sessionData.isEmpty()) {
                return Mono.error(new UnauthorizedException("Session not found or expired"));
            }
            return Mono.just(new SessionData(sessionData));
        });
}

// Logout — immediate revocation
public void logout(String jwt) {
    redisTemplate.delete("session:" + jwt);  // token is immediately invalid
}

Alternative: blocklist pattern (for stateless systems):

// Only store revoked tokens (much smaller set)
public void revokeToken(String jti) {
    long remainingTTL = getRemainingTTL(jwt);  // time until natural expiry
    redisTemplate.opsForValue().set("revoked:" + jti, "1", Duration.ofSeconds(remainingTTL));
}

// Validate
public boolean isRevoked(String jti) {
    return Boolean.TRUE.equals(redisTemplate.hasKey("revoked:" + jti));
}

Trade-offs:


Q7 — What is OWASP Top 10 and which vulnerabilities are most relevant to this system? senior

Answer: OWASP Top 10 is the most widely referenced list of critical web application security risks.

Most relevant to this system:

A01: Broken Access Control

// VULNERABLE: user can access other users' orders
@GetMapping("/orders/{id}")
public Order getOrder(@PathVariable UUID id) {
    return orderRepository.findById(id).orElseThrow();  // no ownership check!
}

// FIXED: verify ownership
@GetMapping("/orders/{id}")
public Order getOrder(@PathVariable UUID id,
                      @RequestHeader("X-User-Id") UUID requestUserId) {
    Order order = orderRepository.findById(id).orElseThrow();
    if (!order.getCustomerId().equals(requestUserId)) throw new ForbiddenException();
    return order;
}

A02: Cryptographic Failures

A03: Injection

A07: Identification and Authentication Failures

A09: Security Logging and Monitoring Failures


Q8 — What is Spring Security's filter chain architecture? senior

Answer: Spring Security is implemented as a chain of Filter implementations. Each request passes through all filters in order.

Default filter order (simplified):

Request → DisableEncodeUrlFilter
        → SecurityContextPersistenceFilter
        → LogoutFilter
        → UsernamePasswordAuthenticationFilter (if form login)
        → BasicAuthenticationFilter (if basic auth)
        → BearerTokenAuthenticationFilter (if OAuth2/JWT)
        → ExceptionTranslationFilter
        → FilterSecurityInterceptor (authorization)
        → Controller

Custom filter for JWT validation (this system's api-gateway):

@Component
@Order(1)
public class JwtAuthenticationFilter implements WebFilter {

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
        String path = exchange.getRequest().getPath().value();

        // Skip auth for public endpoints
        if (path.startsWith("/auth/")) return chain.filter(exchange);

        String authHeader = exchange.getRequest().getHeaders().getFirst(HttpHeaders.AUTHORIZATION);
        if (authHeader == null || !authHeader.startsWith("Bearer ")) {
            exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
            return exchange.getResponse().setComplete();
        }

        String jwt = authHeader.substring(7);
        return sessionService.validateToken(jwt)
            .flatMap(session -> {
                ServerHttpRequest mutated = exchange.getRequest().mutate()
                    .header("X-User-Id", session.userId())
                    .header("X-User-Email", session.email())
                    .header("X-User-Role", session.role())
                    .build();
                return chain.filter(exchange.mutate().request(mutated).build());
            })
            .onErrorResume(e -> {
                exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
                return exchange.getResponse().setComplete();
            });
    }
}

Q9 — What is CSRF and why is it disabled for REST APIs? junior

Answer: CSRF (Cross-Site Request Forgery) is an attack where a malicious website tricks a logged-in user's browser into making unauthorized requests to another site.

How CSRF works:

  1. User logs in to bank.com — browser stores session cookie.
  2. User visits evil.com which has a hidden form: <form action="bank.com/transfer" method="POST">.
  3. Browser automatically sends the bank's session cookie → unauthorized transfer executes.

Why CSRF protection is needed for session-cookie auth: The browser automatically sends cookies with cross-origin requests.

Why CSRF is NOT needed for JWT/header auth (this system):

@Configuration
public class SecurityConfig {
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        return http
            .csrf(csrf -> csrf.disable())  // Safe for JWT — JWT is in Authorization header
            // Cookies are NOT used → cross-origin requests can't include the JWT
            // Evil page cannot read or forge the Authorization header
            .sessionManagement(s -> s.sessionCreationPolicy(STATELESS))
            .build();
    }
}

When to keep CSRF enabled: Traditional web apps using session cookies and form submission.


Q10 — What is CORS and what are the security implications? senior

Answer: CORS (Cross-Origin Resource Sharing) is a browser mechanism that restricts cross-origin HTTP requests. Browsers block responses from a different origin unless the server explicitly allows it via CORS headers.

CORS headers:

Access-Control-Allow-Origin: https://frontend.example.com  (not wildcard for auth!)
Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS
Access-Control-Allow-Headers: Authorization, Content-Type, X-Requested-With
Access-Control-Max-Age: 3600

Spring Cloud Gateway CORS configuration (this system):

spring:
  cloud:
    gateway:
      globalcors:
        cors-configurations:
          '[/**]':
            allowedOrigins:
              - "https://frontend.example.com"  # explicit origin, NOT *
            allowedMethods: ["GET", "POST", "PUT", "DELETE", "OPTIONS"]
            allowedHeaders: ["Authorization", "Content-Type"]
            allowCredentials: true
            maxAge: 3600

Security pitfall: allowedOrigins: "*" with allowCredentials: true is invalid (browsers block it). Never use * when sending cookies or Authorization headers.

Preflight: Browser sends an OPTIONS request before the actual request to check CORS permissions. The server must respond with appropriate headers for the actual request to proceed.


Q11 — How does BCrypt work and why is it preferred for password storage? junior

Answer: BCrypt is a password hashing function designed to be slow (adaptive cost), making brute-force attacks computationally expensive.

Properties:

Spring Security BCrypt:

@Bean
public PasswordEncoder passwordEncoder() {
    return new BCryptPasswordEncoder(12);  // strength 12 — ~200ms per hash (good balance)
}

// Registration
String hashedPassword = passwordEncoder.encode(rawPassword);
// Store hashedPassword in DB

// Login
boolean valid = passwordEncoder.matches(rawPassword, storedHashedPassword);

BCrypt hash format:

$2a$12$R9h/cIPz0gi.URNNX3kh2OPST9/PgBkqquzi.Ss7KIUgO2t0jWMUW
 ^  ^  ^
 |  |  |__ 22-char salt + 31-char hash
 |  |_____ cost factor (12)
 |________ BCrypt version (2a)

Why NOT MD5/SHA: Fast (millions per second with GPU), no salt by default, susceptible to rainbow table attacks.


Q12 — How does JPA prevent SQL injection? junior

Answer: SQL injection occurs when user input is embedded directly into SQL strings, allowing attackers to alter the query structure.

Vulnerable (string concatenation):

// NEVER do this
String sql = "SELECT * FROM users WHERE email = '" + userInput + "'";
// Input: ' OR '1'='1 → returns all users!
// Input: '; DROP TABLE users; -- → destroys the table!

JPA/Hibernate uses parameterized queries automatically:

// Spring Data JPA method names → parameterized query internally
Optional<User> findByEmail(String email);
// Generates: SELECT * FROM users WHERE email = ? (? is bound safely)

// JPQL with named parameters
@Query("SELECT u FROM User u WHERE u.email = :email AND u.status = :status")
Optional<User> findActiveUser(@Param("email") String email, @Param("status") UserStatus status);
// ? binding prevents injection

Native queries (still safe with ? binding):

@Query(value = "SELECT * FROM users WHERE email = :email", nativeQuery = true)
Optional<User> findByEmailNative(@Param("email") String email);
// Still uses parameterized binding — SAFE

Remaining risk: Never concatenate parameters into native queries:

// DANGEROUS even in Spring Data JPA
@Query(value = "SELECT * FROM users ORDER BY " + sortField, nativeQuery = true)  // BAD!
// Fix: whitelist allowed sort columns, never pass raw user input as column name

Q13 — What are secure HTTP headers and how do you add them in Spring Boot? junior

Answer: Secure HTTP response headers protect against various browser-based attacks.

Header Purpose Example Value
Strict-Transport-Security (HSTS) Force HTTPS max-age=31536000; includeSubDomains
X-Content-Type-Options Prevent MIME sniffing nosniff
X-Frame-Options Prevent clickjacking (iframe embedding) DENY or SAMEORIGIN
Content-Security-Policy (CSP) Restrict script/resource sources default-src 'self'
Referrer-Policy Control referer header strict-origin-when-cross-origin
Permissions-Policy Disable browser features geolocation=(), camera=()

Spring Security adds many by default. Customize in SecurityFilterChain:

http.headers(headers -> headers
    .frameOptions(frame -> frame.deny())
    .contentSecurityPolicy(csp -> csp
        .policyDirectives("default-src 'self'; script-src 'self'; frame-ancestors 'none'"))
    .httpStrictTransportSecurity(hsts -> hsts
        .maxAgeInSeconds(31536000)
        .includeSubDomains(true))
);

REST API headers (Spring Security defaults): X-Content-Type-Options: nosniff, X-Frame-Options: DENY, and Cache-Control: no-cache, no-store are applied automatically.


Q14 — What is rate limiting as a security control and how is it implemented? senior

Answer: Rate limiting prevents brute force attacks, credential stuffing, and DoS/DDoS by restricting the number of requests per user/IP per time window.

Security use cases:

This system's rate limiting (api-gateway, token bucket):

spring:
  cloud:
    gateway:
      routes:
        - id: user-service
          filters:
            - name: RequestRateLimiter
              args:
                redis-rate-limiter.replenishRate: 10   # 10 requests/second
                redis-rate-limiter.burstCapacity: 20   # burst up to 20
                key-resolver: "#{@userKeyResolver}"    # rate limit per user
@Bean
KeyResolver userKeyResolver() {
    return exchange -> Mono.justOrEmpty(
        exchange.getRequest().getHeaders().getFirst("X-User-Id")
    ).defaultIfEmpty("anonymous");
}

Response headers:

X-RateLimit-Remaining: 5
X-RateLimit-Replenish-Rate: 10
HTTP/1.1 429 Too Many Requests
Retry-After: 1

Q15 — What is IDOR (Insecure Direct Object Reference) and how do you prevent it? senior

Answer: IDOR (A01: Broken Access Control) occurs when an application exposes internal object IDs that an attacker can manipulate to access resources belonging to other users.

Vulnerable example:

// IDOR: GET /orders/123 — user A can access user B's order by guessing the ID
@GetMapping("/orders/{orderId}")
public Order getOrder(@PathVariable UUID orderId) {
    return orderRepository.findById(orderId).orElseThrow();  // no ownership check!
}

Fixed example:

@GetMapping("/orders/{orderId}")
public Order getOrder(@PathVariable UUID orderId,
                      @RequestHeader("X-User-Id") String userId) {
    Order order = orderRepository.findById(orderId)
        .orElseThrow(() -> new ResourceNotFoundException("Order not found"));

    // Verify ownership — user can only see their own orders
    if (!order.getCustomerId().toString().equals(userId)) {
        throw new ForbiddenException("Access denied");  // or return 404 to avoid leaking existence
    }

    return order;
}

UUID vs sequential IDs: UUIDs are harder to enumerate (not sequential), but are NOT a security control — never rely on ID unpredictability as a security measure. Always check ownership.

Spring Security @PreAuthorize (role + ownership):

@PreAuthorize("@orderSecurityService.isOwner(#orderId, authentication.principal)")
@GetMapping("/orders/{orderId}")
public Order getOrder(@PathVariable UUID orderId) { ... }

Q16 — What is the principle of least privilege and how is it applied in this system? junior

Answer: Principle of least privilege: grant only the minimum permissions required to perform a task. Reduce attack surface by not granting unnecessary access.

Applied at different layers:

Database level:

-- Each service has its own DB user with limited permissions
CREATE USER order_service_user WITH PASSWORD '...';
GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO order_service_user;
-- NOT: GRANT ALL PRIVILEGES / SUPERUSER
-- NOT: shared DB user across all services

Kafka level:

order-service: WRITE to order.created, READ from payment.processed
payment-service: READ from order.created, WRITE to payment.processed
-- Not all services can write to all topics

Application level (Spring Security):

http.authorizeHttpRequests(auth -> auth
    .requestMatchers(HttpMethod.GET, "/api/v1/products/**").permitAll()  // public
    .requestMatchers(HttpMethod.POST, "/api/v1/orders/**").hasRole("USER")
    .requestMatchers(HttpMethod.DELETE, "/api/v1/orders/**").hasRole("ADMIN")
    .anyRequest().authenticated()
);

Container level: Containers run as non-root users. Read-only filesystem where possible.


Q17 — How do you implement audit logging for security events? senior

Answer: Audit logging records security-relevant events for forensic investigation, compliance, and anomaly detection.

Security events to log:

Implementation (Spring AOP + SLF4J):

@Aspect
@Component
public class SecurityAuditAspect {

    private static final Logger auditLog = LoggerFactory.getLogger("AUDIT");

    @Around("@annotation(audited)")
    public Object auditMethod(ProceedingJoinPoint pjp, Audited audited) throws Throwable {
        String userId = SecurityContextHolder.getContext().getAuthentication() != null
            ? SecurityContextHolder.getContext().getAuthentication().getName()
            : "anonymous";

        try {
            Object result = pjp.proceed();
            auditLog.info("action={} user={} status=SUCCESS", audited.action(), userId);
            return result;
        } catch (Exception e) {
            auditLog.warn("action={} user={} status=FAILURE reason={}", audited.action(), userId, e.getMessage());
            throw e;
        }
    }
}

// Usage
@Audited(action = "ORDER_CREATE")
public Order createOrder(CreateOrderRequest request) { ... }

Structured audit log entry (JSON):

{
  "timestamp": "2026-01-15T10:30:00Z",
  "level": "INFO",
  "logger": "AUDIT",
  "action": "LOGIN_FAILED",
  "userId": null,
  "email": "user@test.com",
  "ipAddress": "192.168.1.100",
  "userAgent": "Mozilla/5.0...",
  "reason": "Invalid password",
  "traceId": "abc123"
}

Q18 — What is information disclosure in error messages and how do you prevent it? junior

Answer: Information disclosure (OWASP A05) occurs when error messages reveal sensitive implementation details (stack traces, DB schema, internal IPs) that help attackers.

Vulnerable (Spring Boot default without configuration):

{
  "timestamp": "2026-01-15T10:30:00.000+00:00",
  "status": 500,
  "error": "Internal Server Error",
  "trace": "org.postgresql.util.PSQLException: ERROR: column \"emaill\" of relation \"users\" does not exist\n\tat org.postgresql.jdbc.PgPreparedStatement...",
  "path": "/api/v1/users"
}

This exposes: table name users, column name emaill, PostgreSQL version, internal class names.

Secure error handling:

@RestControllerAdvice
public class GlobalExceptionHandler {

    private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class);

    @ExceptionHandler(Exception.class)
    public ResponseEntity<ErrorResponse> handleUnexpected(Exception ex, HttpServletRequest req) {
        // Log full details internally
        log.error("Unexpected error on {} {}", req.getMethod(), req.getRequestURI(), ex);

        // Return generic message to client
        return ResponseEntity.status(500)
            .body(new ErrorResponse("INTERNAL_ERROR", "An unexpected error occurred"));
    }

    @ExceptionHandler(ResourceNotFoundException.class)
    public ResponseEntity<ErrorResponse> handleNotFound(ResourceNotFoundException ex) {
        return ResponseEntity.status(404)
            .body(new ErrorResponse("NOT_FOUND", ex.getMessage()));
    }
}

Spring Boot configuration (disable full error details):

server:
  error:
    include-stacktrace: never
    include-message: never   # Spring Boot 2.3+ hides messages by default
    include-binding-errors: never

Q19 — How do you securely manage JWT secrets and rotate them? senior

Answer: JWT signing secrets are critical — anyone with the secret can forge valid tokens.

Secret requirements:

Generating a secure secret:

# Generate 512-bit (64-byte) random secret
openssl rand -base64 64
# Or with Java
SecureRandom.getInstanceStrong().nextBytes(new byte[64])

Configuration (environment variable, not in application.yml):

# application.yml
jwt:
  secret: ${JWT_SECRET}  # injected from environment / secrets manager
  expiration: 3600       # seconds

# application-test.yml (test-only secret)
jwt:
  secret: test-secret-key-minimum-256-bits-long-for-hmac-sha256

Secret rotation strategy:

  1. Generate new secret.
  2. Deploy all services with BOTH old and new secrets (accept either).
  3. Issue all new tokens with new secret.
  4. Wait for old tokens to expire naturally (TTL = 1800s in this system).
  5. Remove old secret from configuration.

For zero-downtime rotation: Use RS256 — publish new public key in a JWKS endpoint. Services update the JWKS periodically. Rotate the private key without downtime.


Q20 — What is the difference between authentication and authorization? junior

Answer:

Authentication (AuthN) Authorization (AuthZ)
Question "Who are you?" "What are you allowed to do?"
Mechanism JWT verification, session lookup Role checks, permission checks
When First, before authorization After authentication
This system Redis session lookup in api-gateway X-User-Role header checked by downstream services

This system's flow:

Authentication: api-gateway checks Redis → "this JWT belongs to userId=123, role=USER" ✓
Authorization:  order-service checks "is role USER allowed to create orders?" ✓
                order-service checks "does userId=123 own orderId=456?" ✓

Spring Security:

// Authentication — verify who you are
http.addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class);

// Authorization — verify what you can do
http.authorizeHttpRequests(auth -> auth
    .requestMatchers(POST, "/orders").hasRole("USER")         // authenticated users can order
    .requestMatchers(DELETE, "/orders/**").hasRole("ADMIN")   // only admins can delete
    .requestMatchers(GET, "/actuator/**").hasRole("ADMIN")    // actuator for admins only
);

Q21 — How do you prevent brute force attacks on the login endpoint? senior

Answer: Brute force attacks try many password combinations. Without protection, an attacker can eventually find the correct password.

Defense-in-depth approach:

1. Rate limiting (this system — token bucket):

# In api-gateway, stricter limits for auth endpoints
- id: login-route
  uri: lb://user-service
  predicates:
    - Path=/auth/login
  filters:
    - name: RequestRateLimiter
      args:
        redis-rate-limiter.replenishRate: 5    # 5 attempts per second
        redis-rate-limiter.burstCapacity: 5    # no burst for login
        key-resolver: "#{@ipKeyResolver}"      # rate limit per IP

2. Account lockout (after N failures):

@Transactional
public User attemptLogin(String email, String password) {
    User user = userRepo.findByEmail(email).orElseThrow(() -> new UnauthorizedException("Invalid credentials"));

    if (user.getFailedAttempts() >= 5 && user.getLockoutUntil().isAfter(Instant.now())) {
        throw new AccountLockedException("Account locked. Try again after " + user.getLockoutUntil());
    }

    if (!passwordEncoder.matches(password, user.getPasswordHash())) {
        user.incrementFailedAttempts();
        if (user.getFailedAttempts() >= 5) user.lockFor(Duration.ofMinutes(15));
        userRepo.save(user);
        throw new UnauthorizedException("Invalid credentials");  // same message as "user not found"
    }

    user.resetFailedAttempts();
    return user;
}

3. Constant-time responses: Always check the password hash even if user not found (prevents timing attacks):

if (user == null) {
    passwordEncoder.matches(password, "$2a$12$dummy_hash_to_prevent_timing_attack");
    throw new UnauthorizedException("Invalid credentials");
}

Q22 — What is the jjwt 0.12.x API and how does it differ from older versions? junior

Answer: jjwt 0.12.x introduced a new, fluent builder API and deprecated older methods.

Old API (0.11.x):

// DEPRECATED
String token = Jwts.builder()
    .setSubject(userId)
    .setExpiration(expiry)
    .signWith(SignatureAlgorithm.HS256, secretBytes)
    .compact();

Jws<Claims> jws = Jwts.parserBuilder()
    .setSigningKey(secretBytes)
    .build()
    .parseClaimsJws(token);

New API (0.12.x) (this system):

// Build token
SecretKey key = Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8));

String token = Jwts.builder()
    .subject(userId)                  // was .setSubject()
    .issuedAt(new Date())             // was .setIssuedAt()
    .expiration(expiry)               // was .setExpiration()
    .claim("role", role)
    .signWith(key, Jwts.SIG.HS256)   // was SignatureAlgorithm.HS256
    .compact();

// Parse and verify token
Claims claims = Jwts.parser()         // was .parserBuilder()
    .verifyWith(key)                  // was .setSigningKey()
    .build()
    .parseSignedClaims(token)         // was .parseClaimsJws()
    .getPayload();                    // was .getBody()

String userId = claims.getSubject();
String role = claims.get("role", String.class);

Key changes: All set* methods → fluent names; Jwts.SIG for algorithm constants; parseSignedClaims instead of parseClaimsJws.


Q23 — How do you store tokens securely on the client side? senior

Answer: Token storage on the client affects security against XSS (Cross-Site Scripting) and CSRF attacks.

Storage options:

Storage XSS risk CSRF risk Recommendation
localStorage High (JS can read) None (not sent automatically) Avoid for sensitive tokens
sessionStorage High (JS can read) None Slightly better (cleared on tab close)
httpOnly Cookie None (JS cannot read) Requires CSRF protection Best for web apps
In-memory (JS variable) Medium (page reload = logged out) None Good for SPAs

This system (API-first design — no web UI):

Best practices for web SPAs:

Access token:  In-memory JavaScript variable (lost on page refresh)
Refresh token: httpOnly cookie (not accessible to JS, CSRF-protected)

On page load:  Use refresh token (from httpOnly cookie) to get new access token

What NOT to do:


Q24 — What is a security context in Spring Security and how is it propagated? senior

Answer: SecurityContext holds the current user's Authentication object. Spring Security stores it in SecurityContextHolder (thread-local by default).

// Get current user from SecurityContext
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
String userId = auth.getName();                          // "sub" claim
Collection<?> roles = auth.getAuthorities();             // ["ROLE_USER"]

This system's pattern (downstream services — no JWT, uses injected headers):

// SecurityConfig in order-service — trusts X-User-Id from gateway
@Configuration
public class SecurityConfig {

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        return http
            .addFilterBefore(new TrustedHeaderAuthFilter(), UsernamePasswordAuthenticationFilter.class)
            .csrf(csrf -> csrf.disable())
            .build();
    }
}

// Extract user from trusted gateway headers
public class TrustedHeaderAuthFilter extends OncePerRequestFilter {
    @Override
    protected void doFilterInternal(HttpServletRequest req, HttpServletResponse res, FilterChain chain) {
        String userId = req.getHeader("X-User-Id");
        String role = req.getHeader("X-User-Role");

        if (userId != null) {
            UsernamePasswordAuthenticationToken auth = new UsernamePasswordAuthenticationToken(
                userId, null, List.of(new SimpleGrantedAuthority("ROLE_" + role)));
            SecurityContextHolder.getContext().setAuthentication(auth);
        }

        chain.doFilter(req, res);
        SecurityContextHolder.clearContext();  // always clear after request
    }
}

Q25 — How do you secure microservice-to-microservice communication? senior

Answer: Downstream services trust headers injected by the api-gateway. But what prevents a malicious actor from calling order-service directly with forged X-User-Id headers?

Defense strategies:

1. Network-level restriction: Downstream services are NOT exposed to the public internet — only reachable within the Docker/Kubernetes network. Only api-gateway is exposed.

services:
  order-service:
    # No ports: directive — not accessible from outside Docker network
    expose:
      - "8083"    # accessible only within ecommerce-network

  api-gateway:
    ports:
      - "8080:8080"  # only the gateway is public

2. mTLS (Mutual TLS): Both client and server authenticate with certificates. Only api-gateway has the cert to call downstream services.

3. Internal API keys: Downstream services require a shared secret header from gateway:

// Gateway injects internal key
headers.add("X-Internal-Api-Key", internalApiKey);

// Order-service validates it
if (!internalApiKey.equals(request.getHeader("X-Internal-Api-Key"))) {
    response.sendError(403, "Forbidden");
}

4. Service mesh (Istio, Linkerd): Automatic mTLS between all services, no code changes needed.


Q26 — What is token-based authentication vs session-based authentication? junior

Answer:

Session-based Token-based (JWT)
State Server-side session store Stateless (token carries data)
Scalability Sticky sessions or shared session store Horizontal scaling without shared store
Revocation Delete session → immediate revocation Need blocklist or Redis (see Q6)
Storage (client) Session cookie (httpOnly) localStorage, sessionStorage, or header
Bandwidth Small (session ID) Larger (token contains claims)
CSRF Cookie-based → needs CSRF protection Header-based → CSRF not needed

This system: Hybrid — uses JWT as a session key in Redis. Combines:

Pure stateless JWT (without Redis):


Q27 — What is OAuth2 and how does it differ from JWT? junior

Answer: OAuth2 is an authorization framework (not a protocol) that enables a third-party application to obtain limited access to a service on behalf of a user, without sharing credentials.

JWT is a token format — not an authorization framework. JWTs can be used within OAuth2 flows (as access tokens) or independently.

OAuth2 roles:

OAuth2 flow (Authorization Code):

1. Client redirects user → Authorization Server (AS)
2. User authenticates and consents at AS
3. AS redirects back with authorization code
4. Client exchanges code for access token (+ refresh token) at AS
5. Client calls Resource Server with access token
6. Resource Server validates token (local JWT validation or token introspection)

This system: Does NOT use OAuth2 — implements its own auth with JWT + Redis. OAuth2 would be appropriate for third-party integrations, social login, or when token delegation is needed.


Q28 — How do you test security configurations in Spring Boot? senior

Answer: Security tests verify that access controls are correctly enforced — not just the happy path.

@WebMvcTest(OrderController.class)
@Import(SecurityConfig.class)  // import real security config
class OrderControllerSecurityTest {

    @Autowired MockMvc mockMvc;
    @MockBean OrderService orderService;
    @MockBean SessionValidationService sessionService;

    // Test unauthenticated access
    @Test
    void shouldReturn401WhenNoAuthHeader() throws Exception {
        mockMvc.perform(get("/api/v1/orders"))
               .andExpect(status().isUnauthorized());
    }

    // Test expired/invalid token
    @Test
    void shouldReturn401WhenTokenNotInRedis() throws Exception {
        when(sessionService.validate(any())).thenReturn(Mono.empty());

        mockMvc.perform(get("/api/v1/orders")
            .header("Authorization", "Bearer expired.jwt.token"))
            .andExpect(status().isUnauthorized());
    }

    // Test insufficient role
    @Test
    void shouldReturn403WhenUserAccessesAdminRoute() throws Exception {
        when(sessionService.validate(any())).thenReturn(Mono.just(new SessionData("user-1", "USER")));

        mockMvc.perform(delete("/api/v1/admin/orders/some-id")
            .header("Authorization", "Bearer valid.user.jwt"))
            .andExpect(status().isForbidden());
    }

    // Test success
    @Test
    void shouldReturn200WhenValidToken() throws Exception {
        when(sessionService.validate(any())).thenReturn(Mono.just(new SessionData("user-1", "USER")));
        when(orderService.getOrders(any())).thenReturn(List.of());

        mockMvc.perform(get("/api/v1/orders")
            .header("Authorization", "Bearer valid.jwt"))
            .andExpect(status().isOk());
    }
}

Q29 — What is header injection and how do you prevent it in the api-gateway? senior

Answer: Header injection occurs when an attacker sends forged downstream headers (e.g., X-User-Id: admin-user) directly to the api-gateway, attempting to impersonate another user.

The vulnerability:

// VULNERABLE: if gateway doesn't strip incoming headers before adding its own,
// a direct request to http://gateway/orders with header "X-User-Id: admin-123"
// would pass that header downstream — privilege escalation!

Prevention in Spring Cloud Gateway:

@Component
public class RemoveTrustedHeadersFilter implements GlobalFilter, Ordered {

    // Strip incoming headers that are set by the gateway
    private static final List<String> GATEWAY_HEADERS = List.of(
        "X-User-Id", "X-User-Email", "X-User-Role", "X-Internal-Api-Key"
    );

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        // Remove any incoming trusted headers from the external request
        ServerHttpRequest sanitized = exchange.getRequest().mutate()
            .headers(headers -> GATEWAY_HEADERS.forEach(headers::remove))
            .build();
        return chain.filter(exchange.mutate().request(sanitized).build());
    }

    @Override
    public int getOrder() { return Ordered.HIGHEST_PRECEDENCE; }  // run first!
}

This ensures that trusted headers are ONLY set by the gateway's own auth filter, never passed through from the client.


Q30 — What is a security design review checklist for a microservices system? senior

Answer: A security design review ensures security controls are in place before deployment.

Authentication & Authorization:

Input Validation:

Transport Security:

Infrastructure:

Operational: