🔐

Spring Security Interview Questions

Authentication, filters, JWT, OAuth2 & production hardening

← Learn this topic from scratch first

Foundations: What Is Security, and Why Should You Care?

Q

What is the difference between Authentication and Authorization?

beginner

Authentication verifies WHO a user is (identity check — e.g., validating username/password or a token's signature). Authorization decides WHAT an authenticated user is allowed to do (permission check — e.g., roles, authorities, ACLs). Authentication always happens first; authorization only makes sense once identity is established. HTTP-wise, a failed authentication returns 401 Unauthorized, while a failed authorization returns 403 Forbidden.

Q

What is a Servlet Filter, and how is it different from a Servlet?

beginner

A Servlet handles a request and generates a response (the end of the pipeline — e.g., DispatcherServlet). A Filter intercepts requests/responses before and after they reach a servlet, and multiple filters can be chained. Filters are reusable, composable, cross-cutting units (logging, compression, security) that don't need to know about each other or about the servlet.

Q

How does Spring Security integrate with the servlet filter chain?

beginner

Spring Boot registers a DelegatingFilterProxy in the servlet container, which forwards every request to a Spring-managed bean called FilterChainProxy. FilterChainProxy in turn runs the request through an ordered list of specialized Spring Security filters (CorsFilter, CsrfFilter, UsernamePasswordAuthenticationFilter, ExceptionTranslationFilter, AuthorizationFilter, etc.) before optionally letting it continue on to your controller.

Q

Why can't a filter directly call spring beans without DelegatingFilterProxy?

beginner

Servlet Filters are instantiated and managed by the servlet container's lifecycle (Tomcat), which has no knowledge of the Spring ApplicationContext. DelegatingFilterProxy is a thin bridging Filter registered with the container whose only job is to fetch the actual filter bean from the Spring context (by name) and delegate to it — this lets Spring-managed, dependency-injected filters participate in the plain servlet filter chain.

Q

What happens if a filter doesn't call chain.doFilter()?

beginner

The request processing stops right there — no further filter, and no servlet/controller, will ever execute for that request. This is exactly how Spring Security filters reject unauthenticated or unauthorized requests early: for example, ExceptionTranslationFilter catches an AuthenticationException and commits a 401 response without ever calling chain.doFilter() further down the chain.

Q

Is Spring Security only for REST APIs, or also for traditional MVC apps?

beginner

Spring Security works for both. For server-rendered MVC/monolithic apps it supports formLogin() with server-side sessions and CSRF-protected forms. For stateless REST APIs consumed by SPAs or mobile apps, it supports stateless session management (SessionCreationPolicy.STATELESS) combined with JWT or OAuth2 bearer tokens, with CSRF typically disabled because there's no browser-managed session cookie to forge.

Q

Why is HTTP 401 sometimes confusingly called 'Unauthorized' when it's actually about authentication?

beginner

This is a historical naming inconsistency in the HTTP specification itself, not a Spring Security quirk. HTTP 401 Unauthorized technically means 'you are not authenticated — please provide valid credentials,' while HTTP 403 Forbidden means 'you are authenticated, but you don't have permission.' Spring Security correctly maps AuthenticationException to 401 and AccessDeniedException to 403, despite the confusing HTTP status text.

Spring Security Architecture: The Filter Chain & Internal Flow

Q

Why must the custom login page URL itself be included in permitAll()?

beginner

If the login page required authentication like any other protected resource, an unauthenticated user trying to reach it would be redirected BACK to the login page they're not allowed to see — an infinite redirect loop. The login page (and typically the CSS/JS it needs) must always be publicly accessible so an unauthenticated user can actually reach it.

Q

What exactly does logout() clean up, and why are all three parts necessary?

beginner

invalidateHttpSession(true) destroys the server-side session object itself; deleteCookies("JSESSIONID") instructs the browser to discard the now-meaningless session cookie; and Spring Security also clears the in-memory SecurityContextHolder for the current thread. Skipping any one of these can leave a stale artifact behind — e.g., forgetting to delete the cookie leaves an invalid cookie sitting in the browser, which is mostly harmless but sloppy and can confuse debugging.

Q

Why use @Profile with separate @Configuration classes instead of one config class with if-statements checking the active environment?

beginner

Separate @Configuration classes annotated with @Profile mean Spring will only ever instantiate the bean whose profile is actually active — the wrong configuration literally cannot be created by accident. A single class with runtime if-logic depends on that logic being correct every time and is a common source of production incidents where a developer's local, deliberately loosened settings accidentally ship to production.

Q

What replaced WebSecurityConfigurerAdapter in Spring Security 6?

beginner

WebSecurityConfigurerAdapter was deprecated in Spring Security 5.7 and fully removed in 6.x. It's replaced by defining one or more @Bean methods returning SecurityFilterChain, configured using the HttpSecurity lambda DSL. This component-based approach fits better with Spring Boot's auto-configuration model and allows multiple independent filter chains for different URL patterns.

Q

What is the role of the AuthenticationManager, and how does ProviderManager fit in?

beginner

AuthenticationManager is the single entry-point interface with one method, authenticate(Authentication). ProviderManager is the default implementation: it holds an ordered list of AuthenticationProvider beans and asks each one, in turn, 'can you handle this Authentication type?' — the first provider that supports the type and successfully authenticates wins. If none succeed, a ProviderNotFoundException or AuthenticationException is thrown.

Q

Can a request pass through multiple SecurityFilterChain beans?

beginner

No — FilterChainProxy picks exactly ONE matching SecurityFilterChain (based on securityMatcher) for a given request and runs only that chain. If you need different rules for /api/** versus /admin/**, you configure two separate SecurityFilterChain beans with different @Order values and matchers, not one chain that both apply to.

Q

What's stored inside the SecurityContextHolder, and how long does it live?

beginner

It holds a SecurityContext object wrapping the current Authentication (principal, credentials, authorities). By default its storage strategy is a ThreadLocal, so it lives only for the duration of the current thread/request unless the app is session-based, in which case Spring Security also persists/restores the SecurityContext to/from the HttpSession via SecurityContextPersistenceFilter (or SecurityContextHolderFilter in Spring Security 6).

Q

Why does Spring Security use an AnonymousAuthenticationToken instead of leaving Authentication null?

beginner

Using a real (but low-privilege) Authentication object for unauthenticated users lets the rest of the framework (authorization checks, method security, logging) treat every request uniformly without constant null-checks — an anonymous user is just a principal with an authority like ROLE_ANONYMOUS rather than a special null case.

Q

What is the difference between AuthenticationEntryPoint and AccessDeniedHandler?

beginner

AuthenticationEntryPoint handles the case where a request is unauthenticated but requires authentication (HTTP 401) — e.g., redirecting to a login page or sending a WWW-Authenticate challenge. AccessDeniedHandler handles the case where a request IS authenticated but lacks sufficient authority (HTTP 403) — e.g., rendering a custom 'access denied' page or JSON error body.

Managing Users: UserDetailsService & UserDetailsManager

Q

What is the difference between UserDetailsService and UserDetailsManager?

intermediatePro

UserDetailsService is a read-only, single-method interface (loadUserByUsername) — the minimum Spring Security needs to authenticate. UserDetailsManager extends it and adds full user-management operations (createUser, updateUser, deleteUser, changePassword) for applications that also need to manage accounts, not just verify them.

Q

When would you choose InMemoryUserDetailsManager over a database-backed approach?

intermediatePro

InMemoryUserDetailsManager is appropriate for demos, learning, automated tests, or very small internal tools with a fixed, rarely-changing set of users. It's inappropriate for any application with real end-user registration, since data is lost on restart and there's no persistence.

Q

Why would a team write a custom UserDetailsService instead of using JdbcUserDetailsManager?

intermediatePro

JdbcUserDetailsManager expects a fixed default schema (users/authorities tables) that rarely matches a real business domain model. A custom UserDetailsService lets you map your own JPA entity (with whatever additional business fields you need) into Spring Security's UserDetails contract, giving full control over the query and mapping logic.

Q

What happens if isEnabled() returns false for a user during login?

intermediatePro

DaoAuthenticationProvider checks account status flags (enabled, non-locked, non-expired, credentials-non-expired) BEFORE comparing passwords. If isEnabled() is false, a DisabledException is thrown immediately, regardless of whether the submitted password is correct — this prevents a disabled account from being usable even if its old password is somehow still known.

Q

Why must you always call passwordEncoder.encode() during user registration?

intermediatePro

PasswordEncoder.matches(raw, encoded) expects the stored value to be an encoded hash. If registration stores a raw plain-text password instead, the stored value no longer looks like a valid hash to the encoder, so every subsequent login attempt will fail matches() even with the exact correct password — this is one of the most common 'why can't users log in after signing up' production bugs.

Q

Can loadUserByUsername() be used with something other than a username, like an email or phone number?

intermediatePro

Yes — the parameter name is just a String identifier; Spring Security doesn't care what business concept it represents. Many production systems pass an email address, employee ID, or phone number into this method; what matters is that your custom implementation correctly resolves that identifier to a unique user record.

Password Security: Encoding, Encryption & Hashing

Q

Why shouldn't you use encryption instead of hashing to store passwords?

intermediatePro

Encryption is reversible given the right key — if the encryption key is ever leaked or compromised, every stored password can be decrypted instantly. Applications never legitimately need to read a user's original password back (only verify a match), so a one-way hash is both sufficient and far safer: even a full database leak doesn't directly expose plain-text passwords.

Q

What's wrong with using plain SHA-256 to hash passwords?

intermediatePro

SHA-256 is a fast, general-purpose hash designed for data integrity (checksums), not for password storage. Its speed is a liability here — modern GPUs can compute billions of SHA-256 hashes per second, making brute-force and dictionary attacks against leaked hashes fast and cheap. Purpose-built password hashes like BCrypt/Argon2 are deliberately slow and often memory-hard, making large-scale cracking far more expensive.

Q

What problem does a salt solve, and how does BCryptPasswordEncoder handle it?

intermediatePro

Without a salt, two users with the identical password produce identical hashes, and attackers can precompute a rainbow table mapping common passwords to their hashes once, then instantly reverse any matching leaked hash. A salt is random, unique-per-password data mixed in before hashing, so identical passwords produce different hashes. BCryptPasswordEncoder generates and embeds a random salt automatically inside the resulting hash string, so you never manage salts manually.

Q

What is DelegatingPasswordEncoder and why is it the Spring Security default?

intermediatePro

It's a PasswordEncoder that stores an algorithm-identifying prefix (like {bcrypt} or {argon2}) alongside every hash, and picks the right underlying encoder when verifying. This lets an application support multiple hashing algorithms simultaneously — critical for migrating from an older/weaker algorithm to a newer one without breaking existing users' ability to log in with their current hash.

Q

How would you migrate all users from BCrypt to Argon2 without forcing a mass password reset?

intermediatePro

You can't re-hash without the original plain-text password, so migration typically happens lazily: keep DelegatingPasswordEncoder configured with Argon2 as the default encoder for NEW hashes, but still able to verify old {bcrypt} hashes. On each successful login, check if the stored hash uses the old algorithm (upgradeEncoding()) and if so, re-hash the just-verified raw password with the new encoder and save it — over time, active users migrate transparently.

Q

What does CompromisedPasswordChecker protect against that PasswordEncoder doesn't?

intermediatePro

PasswordEncoder only handles safely STORING and VERIFYING a password; it says nothing about whether the chosen password is inherently weak or already known to attackers. CompromisedPasswordChecker checks a candidate password (at registration/change time) against known breached-password datasets (via k-anonymity so the raw password isn't transmitted), letting you reject passwords like 'password123' even though they'd hash and store 'successfully' otherwise.

Custom AuthenticationProvider & Exception Handling

Q

When would you write a custom AuthenticationProvider instead of relying on DaoAuthenticationProvider?

intermediatePro

When your authentication logic doesn't fit the standard 'fetch UserDetails, compare hashed password' pattern — for example, OTP-based login, verifying against an external legacy identity system, or layering extra business rules (like blocking login during a maintenance window) into the authentication decision itself.

Q

What are the two methods every AuthenticationProvider must implement, and what does each do?

intermediatePro

authenticate(Authentication) performs the actual verification and returns a fully-populated, authenticated Authentication object on success (or throws AuthenticationException on failure). supports(Class<?>) tells ProviderManager whether this provider is capable of handling a given Authentication implementation type, so ProviderManager can pick the right provider without hardcoded type-checking logic.

Q

What is the difference between AuthenticationEntryPoint and AccessDeniedHandler, and which HTTP status does each typically produce?

intermediatePro

AuthenticationEntryPoint handles AuthenticationException (the caller isn't authenticated at all) and typically results in HTTP 401 Unauthorized. AccessDeniedHandler handles AccessDeniedException (the caller IS authenticated but lacks sufficient permission) and typically results in HTTP 403 Forbidden. Both are invoked centrally by ExceptionTranslationFilter.

Q

Why is it dangerous to return the raw exception message from a login endpoint?

intermediatePro

Detailed messages like 'no account found for this email' versus 'incorrect password' let an attacker distinguish between a non-existent account and a wrong password — enabling user enumeration, where an attacker can build a list of valid registered emails/usernames purely from response differences. Best practice: return one generic message for all authentication failures, and log full detail only on the server.

Q

If two AuthenticationProvider beans both support UsernamePasswordAuthenticationToken, what happens?

intermediatePro

ProviderManager iterates the configured provider list in order. Each supporting provider gets a chance to authenticate; if one throws a definitive failure exception like BadCredentialsException, ProviderManager typically stops and propagates that failure rather than silently falling through to the next provider (though behavior can be tuned) — so provider ORDER and exception semantics both matter when multiple providers could apply.

Session Management: Timeout, Concurrency & Fixation

Q

What is a session fixation attack, and how does Spring Security prevent it by default?

intermediatePro

It's an attack where an attacker gets the victim to authenticate under a session ID the attacker already knows (planted beforehand), letting the attacker reuse that same ID to appear as the authenticated victim. Spring Security defaults to changeSessionId(), issuing a fresh session ID immediately upon successful authentication, so any pre-login session ID an attacker might have set up becomes useless after login.

Q

What's required to make maximumSessions() work correctly?

intermediatePro

You must register an HttpSessionEventPublisher bean (a ServletContextListener) so that Spring Security's SessionRegistry is correctly notified when sessions are destroyed (e.g., via logout or timeout). Without it, the registry loses track of destroyed sessions and can incorrectly count/expire sessions.

Q

What does SessionCreationPolicy.STATELESS actually do, and when should you use it?

intermediatePro

It instructs Spring Security to never create or rely on an HttpSession for authentication — every request must independently prove its identity (typically via a JWT in the Authorization header). It's the standard choice for REST APIs consumed by SPAs/mobile clients and for horizontally-scaled microservices, since it removes any need for sticky sessions or a shared session store.

Q

How would you handle sessions in a load-balanced, multi-instance deployment using stateful (session-cookie) authentication?

intermediatePro

Either configure sticky sessions at the load balancer (always routing a given client to the same backend instance) or externalize session storage using something like Spring Session backed by Redis, so any instance can serve any request by reading the shared session store — the latter is generally preferred for resilience, since sticky sessions break if that one instance goes down.

Q

What is the difference between session timeout and concurrent session control?

intermediatePro

Session timeout invalidates a session automatically after a period of INACTIVITY, regardless of how many sessions exist. Concurrent session control limits how many ACTIVE sessions a single user can have at once, regardless of how recently each was used — the two are independent settings addressing different risks (idle abandoned sessions vs. simultaneous multi-device logins).

CORS: Cross-Origin Resource Sharing

Q

What is CORS, and what problem does it solve?

intermediatePro

CORS (Cross-Origin Resource Sharing) is a browser-enforced mechanism that allows a server to explicitly declare which other origins (scheme+host+port combinations) are permitted to make requests to it and read the response. It relaxes the browser's default Same-Origin Policy in a controlled, server-approved way, enabling legitimate cross-origin use cases like a SPA on one port calling an API on another.

Q

What triggers a CORS preflight (OPTIONS) request, and what happens if it fails?

intermediatePro

A preflight is triggered for 'non-simple' requests — those using methods other than GET/HEAD/POST-with-simple-content-type, or including custom headers like Authorization. If the server doesn't respond to the OPTIONS request with the correct Access-Control-Allow-* headers, the browser blocks the actual request entirely and it never reaches your controller — even though tools that don't enforce CORS (like Postman) would succeed.

Q

Why does @CrossOrigin on a controller sometimes still fail with Spring Security enabled?

intermediatePro

Spring Security's filter chain processes (and can reject) requests BEFORE they ever reach your Spring MVC controller, where @CrossOrigin is evaluated. If Spring Security itself isn't configured with a matching CorsConfigurationSource via http.cors(...), the security layer may block or mishandle the request/preflight before MVC-level CORS handling ever gets a chance to run.

Q

Why is allowedOrigins('*') combined with allowCredentials(true) rejected by browsers?

intermediatePro

Allowing every origin to make credentialed (cookie-carrying) requests would let ANY website — including malicious ones — silently issue authenticated requests using a victim's existing session cookie, defeating the entire purpose of CORS as a protective boundary. Browsers enforce that a wildcard origin cannot be combined with credentials; you must list specific allowed origins (or patterns) instead.

Q

Is CORS a substitute for authentication/authorization?

intermediatePro

No — CORS only controls whether a BROWSER will let cross-origin JavaScript read a response; it says nothing about who is allowed to access the data. A properly secured API still needs its own authentication and authorization regardless of CORS configuration, since non-browser clients ignore CORS entirely.

CSRF: Cross-Site Request Forgery

Q

What is a CSRF attack, and why does it work even without the attacker knowing your password?

advancedPro

CSRF exploits the browser's automatic behavior of attaching a site's cookies to every request sent to that site, regardless of what triggered the request. An attacker crafts a page that silently sends a request to the target site; the victim's browser attaches their existing valid session cookie automatically, so the forged request looks authenticated — the attacker never needs to see, steal, or know the victim's actual credentials.

Q

Why is CSRF protection typically unnecessary for stateless JWT-based APIs?

advancedPro

CSRF relies on the browser automatically attaching a cookie the attacker's page can't read or control. A JWT sent via a custom Authorization: Bearer header isn't automatically attached by the browser to cross-site requests — the malicious page has no mechanism to make the victim's browser include that header, so there's no forgeable request to protect against, PROVIDED the token isn't ALSO stored in a cookie that gets sent automatically.

Q

How does a CSRF token actually stop a forged request?

advancedPro

The server issues a unique, hard-to-guess token per session/request and requires it to be present (matching) on every state-changing request. A malicious third-party page cannot read this token (due to Same-Origin Policy preventing it from reading responses/cookies belonging to a different origin), so any forged request it sends is missing the token or has an invalid one, and Spring Security's CsrfFilter rejects it with HTTP 403.

Q

Why does CookieCsrfTokenRepository use withHttpOnlyFalse(), when HttpOnly is usually a security best practice for cookies?

advancedPro

The CSRF token's protection model depends on your OWN frontend JavaScript being able to read the token value from the cookie and echo it back as a request header — this only works if JavaScript CAN read it. Security still holds because a DIFFERENT (attacker) origin cannot read your domain's cookies at all, due to Same-Origin Policy — HttpOnly would only additionally protect against XSS reading it, which is a different threat model from CSRF.

Q

Why doesn't Spring Security's default CSRF protection cover GET requests?

advancedPro

HTTP semantics define GET as a safe, idempotent method that should not have side effects — CSRF protection targets state-changing operations (POST/PUT/DELETE/PATCH). If an application incorrectly implements a destructive operation behind a GET request, that design itself is the vulnerability (also making it trivially exploitable via a simple <img src=...> tag, no form needed), and no amount of CSRF token configuration fixes a fundamentally unsafe use of GET.

Authorization: Roles, Authorities (RBAC) & Custom Filters

Q

What is an AuthorizationDeniedEvent, and how is it different from an authentication failure event?

advancedPro

AuthorizationDeniedEvent fires when an ALREADY-authenticated user is denied access to a specific resource or method they lack permission for (e.g., a failed @PreAuthorize check) — the identity check succeeded, but the permission check failed. This is a distinct and arguably more serious signal than an authentication failure event (Chapter 5), since it represents a known, logged-in identity attempting an action outside their granted permissions, which is a stronger indicator of insider threat or privilege escalation attempts.

Q

Why doesn't an @EventListener for AuthorizationDeniedEvent fire even though the code compiles fine?

advancedPro

Unlike authentication events, which are published automatically, authorization event publishing must be explicitly opted into by exposing an AuthorizationEventPublisher bean (typically a SpringAuthorizationEventPublisher wrapping the ApplicationEventPublisher). Without this bean, authorization checks still work correctly, but no event is ever published, so any listener silently never fires — with no error to indicate why.

Q

What's the actual difference between a Role and an Authority under the hood?

advancedPro

Both are ultimately represented as GrantedAuthority instances — there is no separate Java type for 'role' versus 'authority'. The only distinction is a naming CONVENTION: roles are, by convention, prefixed with ROLE_ and checked via hasRole()/hasAnyRole() (which automatically add/expect that prefix), while authorities have no fixed prefix and represent finer-grained permissions checked via hasAuthority()/hasAnyAuthority().

Q

Why would hasRole('ADMIN') fail even though the user clearly has an 'ADMIN' authority in the database?

advancedPro

hasRole() automatically looks for an authority literally named 'ROLE_ADMIN' — if the database/authority-loading code stores just 'ADMIN' without the ROLE_ prefix, the check will never match. Either store the role correctly prefixed as ROLE_ADMIN, or use hasAuthority('ADMIN') instead, which does no automatic prefixing.

Q

What is the significance of rule ORDER inside authorizeHttpRequests()?

advancedPro

Spring Security evaluates requestMatchers rules in the order they are declared and applies the FIRST one that matches a given request path — subsequent, even more specific, rules are never reached if an earlier broader rule already matched. This is why specific patterns (like /api/admin/**) must be declared before broader catch-all patterns (like /api/**).

Q

What is the difference between addFilterBefore(), addFilterAfter(), and addFilterAt()?

advancedPro

All three register a custom Filter at a specific position relative to an existing named filter class in the chain. addFilterBefore() runs your filter earlier than the reference filter, addFilterAfter() runs it later, and addFilterAt() places it at the same logical position (commonly used to insert a custom token-validation filter where UsernamePasswordAuthenticationFilter would normally sit).

Q

Why should custom filters extend OncePerRequestFilter instead of implementing Filter directly?

advancedPro

Certain servlet container dispatch mechanisms (internal forwards, includes, async dispatches) can cause the same filter to be invoked more than once for what is logically a single client request. OncePerRequestFilter guarantees the filtering logic executes exactly once per request by tracking an internal 'already filtered' marker, preventing duplicate processing (e.g., double logging, double authentication attempts).

Q

What happens if a custom filter forgets to call filterChain.doFilter()?

advancedPro

The request processing stops completely at that filter — no later filter, and no controller/servlet, ever executes. This often manifests as a hung request or a generic timeout with no obvious error message, making it a notoriously tricky bug to diagnose in real projects.

JWT: Token-Based Authentication

Q

Why must a custom JWT-issuing login endpoint call AuthenticationManager.authenticate() instead of comparing passwords itself?

advancedPro

Calling AuthenticationManager.authenticate() reuses the exact same verification pipeline the framework already provides — the configured AuthenticationProvider(s), UserDetailsService, PasswordEncoder, account-status checks (locked/disabled/expired), and authentication event publishing (Chapter 5's audit listeners). Reimplementing password comparison directly in the controller bypasses all of this, duplicating logic and losing consistent exception handling and auditability.

Q

How do you expose the AuthenticationManager as an injectable bean for a custom login controller?

advancedPro

By default, AuthenticationManager isn't directly exposed as a bean. You add a @Bean method that calls AuthenticationConfiguration.getAuthenticationManager(), which returns the fully-configured manager Spring Security builds internally — this is then injectable anywhere, including into a custom REST controller that needs to authenticate credentials manually before issuing a token.

Q

What are the three parts of a JWT, and what is each one for?

advancedPro

Header (metadata: signing algorithm and token type), Payload (the actual claims — subject, roles, issued-at, expiry, custom data), and Signature (a cryptographic signature over the header+payload proving the token hasn't been tampered with). All three are Base64URL-encoded and separated by dots; only the signature provides integrity, not the encoding itself.

Q

Is a JWT payload encrypted? Can anyone read it?

advancedPro

No, by default a JWT payload is only Base64-encoded, not encrypted — anyone who obtains the token (e.g., by pasting it into jwt.io) can decode and read every claim without needing any secret. The secret/private key is only used to verify the SIGNATURE, not to hide the payload's content. Sensitive data should never be placed in JWT claims unless the token is additionally encrypted (JWE).

Q

Why is it hard to revoke a single JWT before it expires?

advancedPro

Because JWT validation is designed to be stateless and local — a resource server checks the signature and expiry without any server-side lookup, so there's no central place to 'delete' one specific token. Mitigations include keeping access tokens short-lived, maintaining a deny-list of explicitly revoked token IDs (jti claim) that resource servers check, or falling back to shorter-lived opaque tokens for highly sensitive operations.

Q

What is the purpose of a refresh token, and why isn't the access token itself just made long-lived?

advancedPro

A refresh token lets a client silently obtain a new access token without forcing the user to re-enter credentials, while keeping the ACCESS token's lifetime short (minimizing the exposure window if it's stolen). Making the access token itself long-lived would mean a single stolen token remains dangerous for a long time; the refresh token is more tightly controlled/revocable and used far less frequently, reducing its exposure.

Q

What's the security risk of storing a JWT in browser localStorage versus an HttpOnly cookie?

advancedPro

localStorage is accessible to any JavaScript running on the page, including malicious scripts injected via an XSS vulnerability — making token theft straightforward if XSS exists anywhere on the site. An HttpOnly cookie can't be read by JavaScript at all, mitigating XSS-based theft, but re-introduces CSRF risk (since browsers auto-attach cookies), which must then be mitigated with CSRF tokens or SameSite cookie attributes.

Q

What's the difference between an opaque token and a JWT (self-contained token) from the resource server's perspective?

advancedPro

An opaque token is a meaningless random string that the resource server must send to the Authorization Server's introspection endpoint to validate and learn its associated claims — this adds a network call per request but allows instant revocation. A JWT is fully self-contained and cryptographically verifiable locally (via signature check), requiring zero network calls per request, but cannot be instantly revoked before its natural expiry.

Method-Level Security

Q

What is the difference between @PreAuthorize and @PostAuthorize?

advancedPro

@PreAuthorize evaluates its SpEL expression BEFORE the method executes — if it fails, the method body never runs at all. @PostAuthorize lets the method run first and evaluates its expression AFTER, with access to the method's return value via returnObject — necessary when the authorization decision depends on data only available after execution, but at the cost of the method's side effects already having occurred even if access is ultimately denied.

Q

Why doesn't @PreAuthorize work when a method calls another annotated method on 'this' (the same object)?

advancedPro

Method security is implemented via Spring AOP proxies wrapping the bean — the proxy intercepts calls made TO the bean from OUTSIDE (through the injected reference), but a call made from within the same object (self-invocation, e.g., this.approve()) goes directly to the real method, bypassing the proxy and therefore bypassing the security check entirely. The common fix is to move the annotated logic into a separate bean and inject/call it externally.

Q

What is the performance risk of @PostFilter, and how do you avoid it?

advancedPro

@PostFilter is applied AFTER the annotated method has already returned its full result set (e.g., every row from a table), filtering the in-memory collection down afterward — meaning the database (or other resource) does the work of fetching data that will just be discarded. The better approach, whenever possible, is to push the filtering condition into the actual query itself (e.g., a repository method scoped by the current user), so the database only returns rows the user is allowed to see in the first place.

Q

How do @PreAuthorize expressions access the current user and method arguments?

advancedPro

SpEL expressions in @PreAuthorize/@PostAuthorize have access to 'authentication' (the current Authentication object, e.g., authentication.name or authentication.principal) and to method parameters directly by name (e.g., #loanId for a parameter named loanId), as well as 'returnObject' in @PostAuthorize specifically for the method's return value.

Q

Why would a team use BOTH URL-level (requestMatchers) and method-level (@PreAuthorize) security together rather than just one?

advancedPro

URL-level rules provide a coarse-grained, centrally-visible first line of defense that's easy to audit at a glance, while method-level annotations enforce fine-grained, business-specific rules directly on the code path that performs the sensitive operation — protecting it regardless of which (possibly future, possibly newly added) controller or code path ends up calling that service method. Relying on only URL rules risks gaps if new entry points bypass the expected controller layer.

OAuth2 & OpenID Connect Deep Dive

Q

How does Spring Security's oauth2Login() simplify implementing 'Login with Google'?

advancedPro

oauth2Login() implements the entire Authorization Code + OIDC flow internally — redirecting to the provider's authorization endpoint (via the conventional /oauth2/authorization/{registrationId} URL), handling the callback, exchanging the code for tokens, validating the ID token, and populating an authenticated OAuth2AuthenticationToken/OidcUser principal — all from a few configuration properties (client-id, client-secret, scopes) rather than implementing any of the OAuth2 protocol steps by hand.

Q

Why might code that reads Authentication.getPrincipal() break for a user who logs in via Google instead of a normal username/password form?

advancedPro

Username/password login populates a UserDetails principal, while oauth2Login() populates a different type — OAuth2User (or OidcUser for OIDC providers like Google). Code that assumes the principal is always castable to UserDetails will throw a ClassCastException the first time it runs for a socially-authenticated user; applications supporting both login methods need to handle both principal types explicitly.

Q

What problem does OAuth2 solve that plain username/password sharing doesn't?

advancedPro

OAuth2 lets a user grant a third-party application LIMITED, SCOPED, and REVOCABLE access to their resources on another service, without ever sharing their actual account password with that third-party app. This avoids giving the third-party unlimited, permanent access and lets the user revoke just that one app's access independently.

Q

Why does the Authorization Code grant use an intermediate 'code' instead of returning the access token directly?

advancedPro

The code is passed back to the client through the browser's redirect URL, which can end up in browser history, server logs, or leaked via referrer headers — exposing a powerful access token this way would be risky. The short-lived, single-use code is instead exchanged for the actual token through a direct backend-to-backend call (using the confidential client_secret), a channel the browser and its logs never see.

Q

What is PKCE, and why do mobile apps and SPAs need it?

advancedPro

PKCE (Proof Key for Code Exchange) replaces the need for a static client_secret — which can't be safely embedded in a public client like a mobile app or JS bundle without being extractable — with a dynamically generated, per-attempt secret (the code_verifier/code_challenge pair). This ensures that even if an authorization code is intercepted, an attacker cannot complete the token exchange without also knowing the original verifier.

Q

What is the difference between OAuth2 and OpenID Connect?

advancedPro

OAuth2 is fundamentally an AUTHORIZATION framework — it grants delegated access to resources but doesn't formally define how to establish user identity. OpenID Connect is a thin identity layer built on top of OAuth2 that standardizes AUTHENTICATION, introducing the JWT-based ID Token, the 'openid' scope, and a /userinfo endpoint — 'Sign in with Google' is an OIDC flow riding on top of OAuth2's Authorization Code grant.

Q

When would you use the Client Credentials grant instead of Authorization Code?

advancedPro

Client Credentials is used for machine-to-machine communication where there is no end-user involved at all — e.g., a backend batch job or one microservice calling another. The calling service authenticates as ITSELF using its own client_id/client_secret, receiving a token representing the service, not any particular user.

Q

How does a resource server validate a JWT access token without calling the Authorization Server on every request?

advancedPro

The resource server fetches the Authorization Server's public signing keys once via the JWKS (JSON Web Key Set) endpoint and caches them, then verifies each incoming JWT's signature locally using those cached public keys — since JWTs are self-contained and signature-verifiable, no per-request network round-trip to the Authorization Server is needed (unlike opaque tokens, which require an introspection call every time).

Q

Why are the Implicit grant and Password grant discouraged in modern OAuth2 (OAuth 2.1)?

advancedPro

The Implicit grant returns the access token directly in the URL fragment, exposing it to browser history and referrer leaks with no proof-of-possession — Authorization Code + PKCE achieves the same goal more safely. The Password grant requires the client to directly collect the user's actual credentials, defeating OAuth2's core purpose of never exposing the user's password to the client application; both are removed in the OAuth 2.1 specification.

Keycloak, Spring Authorization Server & Microservices Security

Q

Why do microservice architectures typically centralize authentication in a dedicated Authorization Server rather than each service handling its own login?

expertPro

Centralizing avoids duplicating (and inconsistently maintaining) credential storage, password policies, and MFA/social-login integration across every service. Every microservice becomes a simple 'resource server' that only needs to validate a token (usually via JWKS-based local signature verification), rather than owning the far more complex and risky job of authenticating users directly.

Q

Why is a custom JwtAuthenticationConverter usually needed when integrating Spring Security with Keycloak?

expertPro

Keycloak nests role information inside a custom claim structure (typically realm_access.roles), which doesn't automatically map to Spring Security's expected GrantedAuthority objects. Without a custom converter extracting and wrapping these roles (usually with a ROLE_ prefix) into GrantedAuthority instances, an authenticated user will have no usable authorities from Spring Security's perspective, and role-based checks will always fail.

Q

What is a Keycloak 'Realm', and why might a company use multiple realms?

expertPro

A realm is an isolated namespace containing its own users, roles, clients, and identity-provider configuration. Companies commonly use separate realms per environment (dev/staging/prod) or per distinct customer/tenant organization, ensuring complete isolation of user bases and configuration between them.

Q

Why doesn't a JWT token customizer (like jwtTokenCustomizer()) affect opaque tokens?

expertPro

A JWT customizer runs only during the process of ENCODING a JWT at the Authorization Server — it has no relevance to opaque tokens, which are just random reference strings with no embedded, customizable claims at all. Any role/claim information for opaque tokens must instead be derived from the introspection endpoint's response structure on the resource-server side, which is a different data shape entirely.

Q

What is the key advantage of building your own Authorization Server with Spring Authorization Server versus adopting Keycloak?

expertPro

Spring Authorization Server gives full programmatic control within the familiar Spring ecosystem (Java configuration, Spring Data-backed client/user repositories, custom token customization logic) without deploying and administering a separate, heavier third-party IAM product. The tradeoff is you take on more responsibility for security-critical correctness and keeping up with a still-evolving project, versus Keycloak's mature, battle-tested, feature-rich (MFA, social login, admin console) out-of-the-box offering.

Production Hardening, OWASP Top 10 & Best Practices

Q

What is BOLA, and why doesn't Spring Security's authentication automatically prevent it?

expertPro

BOLA (Broken Object Level Authorization) is when an authenticated user accesses another user's specific resource simply by manipulating an identifier in the request (e.g., changing an account ID in the URL). Authentication only confirms WHO you are; it says nothing about whether the specific resource being requested actually belongs to you. Preventing BOLA requires explicit ownership checks in your business logic — e.g., method-level @PreAuthorize expressions or repository queries scoped to the current user — Spring Security provides the tools, but the ownership rule itself is application-specific and must be written deliberately.

Q

Why is binding @RequestBody directly to a JPA @Entity considered risky (mass assignment)?

expertPro

If a request body is bound directly onto an entity with all its fields (including sensitive ones like 'role' or 'isAdmin'), an attacker can include extra JSON fields in their request that get silently bound onto fields they should never be able to set themselves — potentially escalating their own privileges. The fix is to always bind incoming requests to a purpose-built DTO containing only the fields that SHOULD be user-settable, then map explicitly (and deliberately) onto the entity server-side.

Q

Why should Spring Boot Actuator endpoints never be exposed publicly without protection?

expertPro

Several actuator endpoints (like /env, /heapdump, /configprops) can reveal sensitive configuration, secrets, or memory contents, and /shutdown can let anyone terminate the application. Actuator should generally run on a separate internal management port, restricted to internal networks, and/or explicitly secured with its own authentication rules distinct from the main application's endpoints.

Q

What's the risk of trusting the X-Forwarded-For header for security decisions?

expertPro

X-Forwarded-For is just a regular HTTP header that any client can set to an arbitrary value unless your infrastructure (reverse proxy/load balancer) is configured to strip any client-supplied value and set its own trusted value. Using it naively for IP-based rate limiting or geo-restriction can be trivially bypassed by an attacker simply spoofing the header, unless your edge infrastructure guarantees its integrity.

Q

What is the difference between SAST and DAST, and where does each fit in a CI/CD pipeline?

expertPro

SAST (Static Application Security Testing) analyzes source code without running it, catching issues like insecure coding patterns or known-vulnerable dependencies early, typically as part of the build/CI stage. DAST (Dynamic Application Security Testing) tests a RUNNING application by actually sending crafted requests (like a controlled attacker would), typically run against a staging environment before production release, catching runtime/configuration issues that static analysis can't see.

Scenario Questions

Q

Scenario 1: Your Angular app on localhost:4200 gets a CORS error calling your Spring Boot API on localhost:8080, but Postman works fine calling the same endpoint. Why, and how do you fix it?

advancedPro

Postman doesn't enforce CORS (it's a browser-only mechanism), which is why it works while the browser blocks the call. The fix is server-side: configure a CorsConfigurationSource with the exact allowed origin (http://localhost:4200), allowed methods, and headers, and wire it into Spring Security via http.cors(...) — not just @CrossOrigin on the controller, since Spring Security's filter chain runs first and can independently block the request or its preflight.

Q

Scenario 2: A user reports they can log in but every request afterward returns 401, even though their username/password is correct.

advancedPro

This usually points to a broken SESSION or TOKEN persistence issue rather than credential validation: for session-based auth, check that the session cookie is actually being sent back by the client (common issue: SameSite/secure cookie flags misconfigured for local HTTP testing, or a missing withCredentials on the frontend HTTP client); for JWT, check that the client is actually attaching the Authorization: Bearer header on subsequent requests, and that the JwtValidationFilter is registered correctly in the filter chain before the authorization check.

Q

Scenario 3: Two different microservices (Accounts and Loans) both trust the same Keycloak realm. How do you prevent a token meant for Accounts from being replayed against Loans?

advancedPro

Configure and validate the 'aud' (audience) claim on the JWT for each resource server — each service should reject tokens whose audience doesn't explicitly include itself. This typically requires configuring distinct audiences per client/scope in Keycloak and adding an audience validator on each resource server's JWT decoder.

Q

Scenario 4: A new engineer disables CSRF globally (csrf.disable()) to 'fix' a 403 error they were seeing while testing a POST request from Postman. Is this safe to merge?

advancedPro

Only if the application is genuinely stateless (JWT/OAuth2 bearer-token authentication, no cookie-based session) — for a cookie/session-based application, disabling CSRF removes real protection against forged cross-site requests. The right fix for Postman testing specifically is to correctly send the CSRF token (fetch it first, then include it as a header on subsequent requests) rather than disabling protection outright.

Q

Scenario 5: Your team wants to allow a mobile app to call your API using OAuth2, but the mobile app can't safely store a client_secret. What do you recommend?

advancedPro

Use the Authorization Code grant with PKCE, treating the mobile app as a PUBLIC client (no client_secret at all). PKCE's dynamically generated code_verifier/code_challenge pair protects the exchange without requiring a static, embeddable secret that could be extracted from the compiled app.

Q

Scenario 6: A batch job needs to call an internal microservice API every night with no user involved. Which OAuth2 grant type fits, and how would you configure the resource server?

advancedPro

Client Credentials grant — the batch job authenticates as itself using its own client_id/client_secret, receiving an access token that represents the SERVICE, not a user. The resource server is configured identically to any other OAuth2 resource server (validating the JWT via JWKS or introspection), since it doesn't need to distinguish machine callers from user-driven ones at the token-validation layer.

Q

Scenario 7: Users complain they're randomly logged out when your app is deployed across three load-balanced server instances using traditional session-based authentication. What's happening, and how do you fix it?

advancedPro

Without sticky sessions or a shared session store, a user's session (held in one instance's memory) isn't visible to a different instance the load balancer might route them to on a subsequent request, causing an apparent random logout. Fix with either load-balancer sticky sessions, or better, externalize sessions using Spring Session backed by a shared store like Redis, so any instance can serve any authenticated request.

Q

Scenario 8: An admin panel's 'Delete User' button is hidden in the UI for non-admin users, but a curious user opens dev tools and successfully calls the DELETE endpoint directly. What went wrong, and what's the fix?

advancedPro

The authorization check existed only in the FRONTEND (a hidden button), with no corresponding server-side enforcement — the backend endpoint itself trusted the caller. The fix is to enforce the check on the server, using either a requestMatchers rule restricting the endpoint's HTTP method/path to a required role, and/or a @PreAuthorize annotation directly on the underlying service method, so the rule holds regardless of what UI (or curl command) calls it.

Q

Scenario 9: Right after deployment, every newly registered user can never log in, even immediately after signing up with the exact password they chose.

advancedPro

This is the classic missing passwordEncoder.encode() bug at registration — the raw password is stored as-is, so PasswordEncoder.matches(raw, storedValue) fails during login because the stored value doesn't look like a valid hash. Fix by encoding the password at the single point of entry (registration/password-change) and verify existing broken accounts by forcing a password reset, since their stored values can't be un-corrupted after the fact.

Q

Scenario 10: Login requests are taking 2+ seconds each under moderate load, and profiling points to BCryptPasswordEncoder.matches(). What's happening, and how do you fix it?

advancedPro

BCrypt's 'strength' (work factor) controls how computationally expensive each hash operation is by design — a strength set too high (e.g., 15-16) for the available hardware can make each login take seconds instead of milliseconds. Benchmark and tune the strength value (commonly 10-12) to balance security against acceptable login latency for your actual server hardware, rather than assuming a higher number is always better.

Q

Scenario 11: Two custom AuthenticationProvider beans both support UsernamePasswordAuthenticationToken, and users are being authenticated against the WRONG identity store.

advancedPro

ProviderManager tries providers in the order they are registered and generally uses the first one that successfully authenticates (or throws a hard failure) — if the higher-priority provider happens to also accept credentials meant for a different store (e.g., a test/legacy provider left registered), it can silently 'win' over the intended provider. Fix by explicitly controlling provider order (e.g., building the ProviderManager's provider list explicitly) and removing/disabling providers not meant for the current environment.

Q

Scenario 12: A @PreAuthorize check on a service method is being completely ignored when called from another method in the SAME class.

advancedPro

This is self-invocation: Spring's method security relies on an AOP proxy wrapping the bean, but a call from within the same class instance (this.method()) goes directly to the real object, bypassing the proxy entirely. Fix by moving the annotated method into a separate bean and calling it via injection, or by using AopContext.currentProxy() (requires exposeProxy=true) as a less clean alternative.

Q

Scenario 13: A @PostFilter-protected endpoint listing 'my transactions' has become extremely slow as the transactions table grew into the millions.

advancedPro

@PostFilter runs AFTER the annotated method already fetched the FULL result set, filtering it down in memory afterward — at scale this means fetching millions of rows just to discard most of them. Fix by pushing the filtering condition (e.g., WHERE owner_id = :currentUserId) into the actual repository query, so the database only returns rows the user is allowed to see in the first place.

Q

Scenario 14: After correctly setting up Keycloak with realm roles assigned to a test user, every API call still returns 403 Forbidden in the Spring Boot resource server.

advancedPro

Keycloak nests roles inside a realm_access.roles claim that doesn't automatically map to Spring Security's expected GrantedAuthority objects — without a custom JwtAuthenticationConverter extracting and wrapping these into authorities (typically with a ROLE_ prefix), the authenticated user has zero usable authorities from Spring Security's point of view, so every hasRole()/@PreAuthorize check fails regardless of what's configured in Keycloak's admin console.

Q

Scenario 15: A distributed system with resource servers across two data centers occasionally rejects valid, non-expired JWTs as 'expired' or 'not yet valid'.

advancedPro

This points to clock skew between servers — if the token-issuing Authorization Server's clock and a resource server's clock drift even a few seconds apart, strict exp/iat/nbf validation can incorrectly reject a technically-valid token. Fix by enabling NTP time synchronization across all servers and configuring a small, explicit clock-skew tolerance in the JWT validation library.

Q

Scenario 16: Your monitoring detects the same refresh token being used from two different IP addresses within seconds of each other.

advancedPro

This is the signature of refresh token theft, detectable specifically because of refresh token ROTATION: if an old (already-used-and-replaced) refresh token is presented again, that's evidence someone else obtained a copy of it before or after the legitimate client rotated it. The correct response is to immediately invalidate the entire token family (every descendant token issued from that lineage) and force full re-authentication, rather than just rejecting the single reused token.

Q

Scenario 17: A user leaves a form open in a browser tab for over an hour, then submits it, and gets an unexplained 403 error.

advancedPro

The CSRF token tied to their session/page load has likely expired or been rotated server-side by the time of submission, so the token the browser is sending no longer matches what the server expects. Rather than showing a raw, confusing 403, handle this gracefully client-side: detect the specific CSRF-failure response and prompt the user to refresh the page (fetching a new token) before resubmitting.

Q

Scenario 18: After adding a second SecurityFilterChain bean for an admin subdomain, requests to /api/** that used to work now return 401 unexpectedly.

advancedPro

FilterChainProxy picks exactly ONE matching SecurityFilterChain per request based on @Order and securityMatcher — if the new chain's matcher is too broad (or declared with a lower @Order value, making it evaluated first) it can unintentionally intercept requests meant for the original chain, applying the wrong (stricter) rules. Fix by tightening each chain's securityMatcher to the exact intended URL pattern and setting @Order values deliberately so the more specific chain is evaluated first.

Q

Scenario 19: A security audit flags that /actuator/heapdump and /actuator/env are reachable from the public internet on your production Spring Boot service.

advancedPro

This is a Security Misconfiguration (OWASP): several Actuator endpoints can leak secrets (environment variables, memory contents) or allow disruptive actions if left exposed without protection. Fix by restricting Actuator to a separate internal management port not exposed publicly, and/or explicitly securing sensitive actuator endpoints behind authentication with a dedicated SecurityFilterChain matcher.

Q

Scenario 20: A penetration test shows that submitting {"role":"ADMIN"} in the registration JSON body silently grants the new account admin privileges.

advancedPro

This is a mass-assignment vulnerability caused by binding the incoming @RequestBody directly onto a JPA entity that includes a settable 'role' field — the attacker simply supplies extra fields the endpoint should never have honored. Fix by binding requests to a dedicated DTO containing only the fields a user should legitimately set (never 'role'), and setting sensitive fields like role/isAdmin explicitly and deliberately in server-side code.

Q

Scenario 21: A customer complains they can see another customer's loan details by simply changing the loan ID number in the URL.

advancedPro

This is BOLA/IDOR (Broken Object Level Authorization) — authentication confirmed who the caller is, but nothing checked whether the SPECIFIC loan record actually belongs to them. Fix with an explicit ownership check, e.g., a @PreAuthorize expression comparing the loan's owner to the current principal, or scoping the repository query itself to WHERE customer_id = :currentUserId.

Q

Scenario 22: Your login endpoint is being hit thousands of times per minute from a botnet trying common password lists against real usernames.

advancedPro

This is credential stuffing/a dictionary attack against your login endpoint. Layer defenses: per-IP and per-account rate limiting at the gateway, temporary account lockout after repeated failures (isAccountNonLocked()), a CAPTCHA challenge after a failure threshold, and anomaly monitoring for spikes of failed logins across many accounts from a small set of IPs.

Q

Scenario 23: Your 'Login with Google' button works fine in Chrome but fails silently in Safari with strict cookie settings enabled.

advancedPro

Some browsers' stricter default SameSite/third-party-cookie policies can interfere with certain OAuth2 redirect-based flows that rely on cookies during the authorization/callback round-trip. Mitigate by ensuring your session/state cookies used during the OAuth2 flow are set with appropriate SameSite=Lax (or None with Secure) attributes, and test the full flow across all target browsers, not just Chromium-based ones.

Q

Scenario 24: Integrating with a third-party OAuth2 provider, you keep getting 'invalid redirect_uri' errors even though the URL looks correct.

advancedPro

redirect_uri must match EXACTLY, character-for-character, what's pre-registered with the Authorization Server — a trailing slash, http vs https, or a different port is enough to fail the match. Carefully compare the registered URI and the one your application actually sends (log it) to spot the discrepancy, which is very often a subtle formatting difference.

Q

Scenario 25: A user picks a password that technically satisfies your complexity rules (uppercase, number, symbol) but registration still rejects it.

advancedPro

Your CompromisedPasswordChecker integration (checking against known-breached password datasets via k-anonymity) is very likely rejecting it because that exact password has appeared in a public data breach before, regardless of how 'complex' it looks. This is by design — complexity rules alone don't guarantee a password is actually unpredictable to attackers armed with real breach data.

Q

Scenario 26: A method annotated with @Async that reads SecurityContextHolder.getContext().getAuthentication() gets null instead of the logged-in user.

advancedPro

By default, SecurityContextHolder uses a ThreadLocal strategy, so the authenticated context set on the original request thread isn't automatically visible to a new thread spawned by @Async. Fix by switching the holder strategy to MODE_INHERITABLETHREADLOCAL, or by explicitly capturing and propagating the SecurityContext into the async task yourself.

Q

Scenario 27: After scaling from one server instance to three behind a load balancer, users report being logged out mid-session at random.

advancedPro

With traditional stateful (session-cookie) authentication and no sticky sessions or shared session store, a user's in-memory session on instance A isn't visible if the load balancer routes their next request to instance B. Fix with either load-balancer sticky sessions or, more robustly, externalizing sessions with Spring Session backed by Redis so any instance can serve any authenticated request.

Q

Scenario 28: Your application needs to authenticate a WebSocket connection used for real-time account balance updates.

advancedPro

The WebSocket handshake itself is a normal HTTP request, so standard Spring Security filters (session cookie or Authorization header validation) can protect the initial handshake/upgrade request. After the connection is established, since WebSocket messages don't carry headers per-message the way HTTP requests do, the authenticated principal from the handshake is typically captured once and associated with the WebSocket session for the connection's lifetime.

Q

Scenario 29: A SaaS product wants to guarantee that Company A's users can never see Company B's data, even if both are on the same shared infrastructure.

advancedPro

This calls for tenant isolation enforced through the token itself: embed a tenant_id claim in the JWT/OIDC token at issuance (e.g., a dedicated Keycloak realm per tenant, or a custom claim mapper), and enforce it at BOTH the authorization layer (every query/service method scoped by tenant_id, similar to per-user ownership checks) and, ideally, at the database layer (row-level security or separate schemas) as defense-in-depth beyond just the application code.

Q

Scenario 30: Your team wants to migrate everyone from BCrypt to Argon2 without forcing a mass password reset across the whole user base.

advancedPro

Use DelegatingPasswordEncoder configured with Argon2 as the encoder for new hashes, while it can still verify existing {bcrypt}-prefixed hashes. On each successful login, check encoder.upgradeEncoding() against the stored hash; if it indicates an upgrade is available, re-hash the just-verified raw password with the new algorithm and save it — active users migrate transparently over time as they log in, with no forced reset.

System Design Questions

Q

Design the authentication strategy for a banking application (EazyBank) with a separate Angular frontend and Spring Boot backend microservices.

expertPro

Use OAuth2/OIDC with a central Authorization Server (Keycloak or Spring Authorization Server): the Angular app performs Authorization Code + PKCE against the Auth Server (never touching backend credentials directly), receiving a JWT access token. Each backend microservice (Accounts, Loans, Cards) acts as a stateless resource server, validating the JWT locally via JWKS, with fine-grained authorization enforced through role/authority claims mapped from the token, plus method-level @PreAuthorize checks for ownership rules like 'this account belongs to this user'.

Q

How would you design token refresh so that a stolen refresh token is detected quickly?

expertPro

Implement refresh token ROTATION: every time a refresh token is used, issue a brand-new refresh token and invalidate the old one. If an already-used (old) refresh token is ever presented again, treat it as a signal of theft — immediately invalidate the ENTIRE token family (all descendants of that refresh token) and force re-authentication, since a legitimate client would never replay an already-rotated token.

Q

Design a rate-limiting strategy to protect the login endpoint from credential-stuffing attacks without locking out legitimate users too aggressively.

expertPro

Layer defenses: (1) per-IP and per-account rate limiting at the API gateway/edge (e.g., a sliding window limiting attempts per minute), (2) exponential backoff/temporary lockout after repeated failures on a specific account (via isAccountNonLocked() logic), (3) CAPTCHA challenges triggered after a threshold of failures, and (4) anomaly-based monitoring/alerting (e.g., a sudden spike of failed logins across many different accounts from one IP, which more strongly indicates credential stuffing than a single user's own retries).

Q

How would you secure sensitive data claims so a decoded JWT doesn't leak PII if intercepted?

expertPro

Avoid placing PII (emails, names, SSNs) in JWT claims at all where possible — reference a user ID instead, and let the resource server fetch full profile detail server-side when actually needed. Where identity claims genuinely must travel in the token (as with OIDC ID tokens), consider using JWE (JSON Web Encryption) to encrypt sensitive claims, not just sign them, and always enforce token transport exclusively over TLS.

Q

Design a multi-tenant SaaS authorization model where each customer organization's data must stay fully isolated.

expertPro

Embed a tenant_id claim in every issued token (via a dedicated Authorization Server realm/configuration per tenant, or a custom claim mapper), and enforce it at multiple layers as defense-in-depth: method-level @PreAuthorize/@PostFilter checks scoped by tenant_id, repository queries that always filter by the current tenant, and ideally database-level row-level security or separate schemas so even a code-level bug can't leak cross-tenant data.

Q

Design a step-up authentication flow requiring extra verification (e.g., an OTP) only for high-value operations like a large fund transfer, without forcing re-login for everything.

expertPro

Issue the initial JWT with a claim indicating the authentication 'level' achieved (e.g., acr or a custom step_up_verified claim). Sensitive endpoints use a @PreAuthorize expression checking that claim; if absent, the API returns a specific 'step-up required' response prompting the client to complete an additional OTP challenge, after which a new, elevated token (or an additional short-lived assertion) is issued permitting that specific high-value action.

Q

Design a zero-downtime strategy for rotating the JWT signing key used by your Authorization Server.

expertPro

Publish signing keys via a JWKS endpoint containing MULTIPLE keys, each tagged with a 'kid' (key ID). Introduce the new key alongside the old one first (both published), start signing NEW tokens with the new key while resource servers continue accepting tokens signed by either key (matched via 'kid'), and only remove the old key from JWKS once all tokens signed with it have naturally expired — avoiding any window where valid tokens are suddenly rejected.

Q

Design a safe 'login as user' (admin impersonation) feature for a customer-support team.

expertPro

Issue a distinctly-scoped, short-lived impersonation token containing both the support agent's own identity AND the target user's identity (e.g., acting_as / on_behalf_of claims), rather than simply re-authenticating as the target user. Restrict impersonation tokens from performing destructive/sensitive operations (e.g., changing the user's password or email), require the action to be explicitly re-authorized per session, and audit-log every impersonated action against BOTH identities for accountability.

Q

Design a secure 'forgot password' / reset-token flow resistant to token replay and account enumeration.

expertPro

Generate a single-use, cryptographically random, short-lived reset token (not a predictable value), store only its HASH server-side (so a database leak doesn't expose usable tokens), and invalidate it immediately after first use or upon a new reset request. Return an identical response regardless of whether the submitted email exists, to prevent attackers from enumerating valid registered accounts via response-timing or content differences.

Q

Design a security model for receiving webhooks from a third-party service (e.g., a payment provider) that can't participate in your normal OAuth2/session-based authentication.

expertPro

Webhooks typically can't complete an interactive login flow, so authenticate them differently: verify an HMAC signature the provider computes over the raw request body using a shared secret, sent in a custom header (e.g., X-Signature) — the endpoint recomputes the HMAC and rejects the request on any mismatch. This endpoint should be explicitly excluded from CSRF (no session involved) but must still validate the signature on every single call, and should reject replayed requests using a timestamp + nonce check.

Q

Design the authentication architecture question: should each microservice validate tokens independently, or should a central API gateway handle it once?

expertPro

Both patterns are valid tradeoffs: a central gateway validating tokens once and forwarding trusted internal headers/claims reduces duplicated logic and centralizes policy, but creates a single point that must scale and stay available, and requires the internal network to be trusted (since downstream services no longer independently verify). Each microservice independently validating its own JWT (via cached JWKS) removes that single point of trust and keeps services autonomous, at the cost of duplicating validation configuration across services — many production systems combine both: a gateway for coarse routing/rate-limiting, plus independent validation at each resource server for defense-in-depth.

Q

Design a scalable audit pipeline capturing every authentication and authorization event across a microservices platform for compliance and anomaly detection.

expertPro

Have every service publish AuthenticationSuccessEvent/failure events and AuthorizationDeniedEvent (Chapters 5 and 9) to a central message stream (e.g., Kafka) rather than writing directly to a shared database from every service. A downstream consumer aggregates these into a queryable audit store, feeding both compliance reporting (who accessed what, when) and real-time anomaly detection (e.g., alerting on a spike of AuthorizationDeniedEvents for one user, suggesting probing or a compromised, downgraded-privilege account).

Q

Design a public self-registration endpoint hardened against automated bot abuse without meaningfully hurting real user experience.

expertPro

Layer proportionate defenses: a CAPTCHA (only shown after suspicious velocity, not on every legitimate attempt), per-IP and per-email-domain rate limiting, CompromisedPasswordChecker rejection of known-breached passwords, and email verification before the account is fully activated — combined with monitoring for registration bursts from narrow IP ranges or disposable-email domains, which are strong bot signals distinct from normal registration patterns.

Q

Design a mechanism to detect and respond to suspicious login patterns, such as a login from a new device or an impossible-travel scenario (logins from two distant countries within minutes).

expertPro

On each successful authentication (leveraging the AuthenticationSuccessEvent listener from Chapter 5), record device fingerprint/IP/geolocation alongside the login. Compare against the user's recent login history: a new, never-seen device or a geolocation that's implausible given the time elapsed since the last login triggers a step-up challenge (re-verify via OTP/email) or a proactive security notification to the user, rather than silently allowing or silently blocking the login outright.

Q

Design a backend that must simultaneously support a traditional server-rendered web client (session-cookie based) and a newer mobile app (JWT-based), sharing the same underlying business logic.

expertPro

Configure two separate SecurityFilterChain beans with distinct @Order values and securityMatchers: one scoped to the web UI's routes using formLogin() with SessionCreationPolicy.IF_REQUIRED (or default), and another scoped to /api/** using SessionCreationPolicy.STATELESS with JWT bearer-token validation. Both chains ultimately resolve to the SAME UserDetailsService/business services underneath, so the dual authentication mechanisms are purely a concern of the security configuration layer, not duplicated business logic.