Prepare for Spring Security interviews with questions grouped by experience level, from authentication basics to OAuth2 architecture.
Junior (0-2 years)
A framework for handling authentication and authorization in Spring applications. It sits in front of your application as a chain of filters, checking who a user is and what they're allowed to do before a request ever reaches your controller code. It's the standard choice for securing a Spring Boot application, rather than something teams typically build from scratch.
Authentication verifies who a user actually is, usually by checking a username and password, or validating a token. Authorization decides what that now-identified user is allowed to do. A user can be authenticated but still get denied access to a specific resource if authorization determines they lack the right role or permission.
Every endpoint gets locked down behind basic authentication by default, and Spring Boot generates a random password printed to the console on startup, with a default username of user. This surprises a lot of newcomers the first time, since simply adding the dependency changes the application's behavior immediately, before any custom configuration is written.
A bean defining how Spring Security handles incoming requests, which endpoints require authentication, which are public, what login mechanism to use. In modern Spring Security, you configure this by defining a SecurityFilterChain bean rather than extending an old base configuration class, which was how it used to be done.
Form-based login presents an actual HTML login page and typically uses a session cookie afterward to track the authenticated user. HTTP Basic sends credentials directly in an Authorization header on every single request, with no session or login page involved at all. Basic auth is simple and common for machine-to-machine API calls, while form login fits a browser-based user-facing application.
It signals that a class contains Spring Security configuration and pulls in the necessary web security infrastructure. In many modern Spring Boot setups it's applied automatically once the Spring Security starter is on the classpath, though it's still commonly added explicitly for clarity.
Inside the SecurityFilterChain bean, use authorizeHttpRequests to define matchers, requestMatchers('/public/**').permitAll() opens that path to everyone, while anyRequest().authenticated() requires login for everything else. Order matters here, since Spring Security evaluates these rules in the sequence they're defined.
Create a UserDetailsService bean returning an InMemoryUserDetailsManager populated with a User object, built using the User.builder() API, specifying a username, an encoded password, and roles. This is useful for local development or a quick demo, but a real application backs authentication with an actual database instead.
It's the component responsible for hashing passwords before storing them and verifying a submitted password against that stored hash during login. Spring Security requires one explicitly, since storing or comparing plain-text passwords is exactly the kind of mistake the framework is designed to prevent by default.
BCryptPasswordEncoder is deliberately slow and includes a random salt built into each hash automatically, which resists brute-force and rainbow-table attacks. MD5 is fast and has no built-in salting, which is exactly why it's considered unsuitable for password storage. Speed is a security weakness here, not a benefit.
csrf(csrf -> csrf.ignoringRequestMatchers('/api/**')) excludes matching paths from CSRF checks. This is commonly done for a stateless REST API secured with tokens, since CSRF protection is specifically designed to protect session-cookie-based browser interactions, a threat model that doesn't really apply to a token-authenticated API client.
For a browser-based form login setup, Spring Security redirects the user to the login page. For an API expecting JSON, it typically returns a 401 or 403 status instead, depending on configuration, since redirecting an API client to an HTML login page wouldn't make sense for that kind of consumer.
An interface with a single method, loadUserByUsername, that Spring Security calls to look up a user's details during login, typically by querying a database. Implementing this interface is how you connect Spring Security's authentication process to your actual application's user data instead of relying on an in-memory list.
It represents the core information Spring Security needs about an authenticated user, their username, password hash, granted authorities, and account status flags like whether the account is locked or expired. Your own User entity typically implements this interface, or gets wrapped in a class that does.
The submitted credentials get wrapped in an Authentication object and passed to an AuthenticationManager, which delegates to one or more AuthenticationProviders. The default provider calls your UserDetailsService to load the real user record, then uses the configured PasswordEncoder to check the submitted password against the stored hash, succeeding or failing based on that comparison.
It holds the SecurityContext for the current thread, which in turn holds the Authentication object representing whoever is currently logged in. Calling SecurityContextHolder.getContext().getAuthentication() from anywhere in your application code gives you access to the currently authenticated user's details.
The Principal represents the identity of the authenticated user, typically the username or the UserDetails object itself. An Authority represents a specific permission or role that principal holds, like ROLE_ADMIN. One principal can hold several authorities at once.
Configure a logout handler in the SecurityFilterChain, typically pointing at a /logout endpoint that Spring Security handles automatically, invalidating the session and clearing the security context. For a token-based setup without server-side sessions, logout usually means the client simply discards the token, since there's no session to invalidate on the server side.
An authority is a specific granted permission, stored as a plain string, like READ_PRIVILEGES. A role is a specialized kind of authority, conventionally prefixed with ROLE_, representing a broader grouping, like ROLE_ADMIN. hasRole('ADMIN') in configuration is really just shorthand that checks for the ROLE_ADMIN authority underneath.
requestMatchers('/admin/**').hasRole('ADMIN') inside the SecurityFilterChain configuration restricts that path to users holding the ADMIN role. Anyone without it gets a 403 Forbidden response rather than the request reaching the actual controller.
It lets you apply authorization rules directly on individual methods, rather than only at the URL level, using annotations like @PreAuthorize. You enable it with @EnableMethodSecurity on a configuration class, after which those annotations start being enforced across the application.
@PreAuthorize evaluates a security expression before the method body runs at all, blocking the call entirely if the check fails, rather than letting the method start executing and checking permissions partway through. @PreAuthorize("hasRole('ADMIN')") above a method is both more declarative and less error-prone than scattering manual permission checks throughout the method body itself.
hasRole('ADMIN') automatically checks for the ROLE_ADMIN authority, adding the ROLE_ prefix for you behind the scenes. hasAuthority('ADMIN') checks for the exact string ADMIN with no prefix added. Mixing these up, expecting hasAuthority to add the prefix the way hasRole does, is a common source of a permission check that silently never passes.
A SpEL expression referencing the request's own data works for simple cases, @PreAuthorize("#userId == authentication.principal.id"), comparing a method argument against the authenticated user's ID. For more complex ownership checks, a custom permission evaluator is generally the cleaner, more maintainable approach.
Spring Security itself is implemented as a chain of servlet filters, each handling one specific concern, one for authentication, one for CSRF checks, one for exception handling, and so on. This chain sits in front of your application's own DispatcherServlet, so security processing happens before a request ever reaches your controllers.
It's a servlet filter that delegates its actual work to a Spring-managed bean, letting Spring Security's filter chain participate in the standard servlet filter mechanism while still being configured and managed as ordinary Spring beans. This bridges the plain servlet filter model with Spring's own dependency injection and bean lifecycle.
UsernamePasswordAuthenticationFilter handles form-based login, processing a submitted username and password from a login form's POST request. BasicAuthenticationFilter handles HTTP Basic authentication instead, extracting credentials directly from the Authorization header on every request rather than from a submitted form.
Implement a filter (often extending OncePerRequestFilter for simplicity), then register it in the SecurityFilterChain configuration using addFilterBefore() or addFilterAfter(), specifying exactly where relative to an existing filter it should run. A custom JWT validation filter is a very common real-world example, usually placed before the standard username-password filter.
It guarantees a filter's logic runs exactly once per request, even in setups where a request gets forwarded internally and would otherwise pass through the filter chain more than once. For most custom security filters, this guarantee is exactly what you want, which is why it's the more common base class to extend for custom logic.
Cross-Site Request Forgery tricks a logged-in user's browser into submitting a request they didn't intend to make, relying on the browser automatically sending along the user's existing session cookie. Spring Security protects against this by requiring a valid, unpredictable CSRF token on any state-changing request, a token an attacker's page has no way to know or forge.
CSRF specifically exploits the browser's automatic, implicit inclusion of session cookies on every request. A token-based API instead requires the client to explicitly attach a token to each request, usually in a header, which an attacker's malicious page has no automatic way to obtain or attach on the victim's behalf.
Session fixation is an attack where an attacker gets a victim to authenticate using a session ID the attacker already knows, then uses that same session ID to impersonate them afterward. Spring Security defends against this by regenerating the session ID automatically upon successful login, so any session ID an attacker set beforehand becomes worthless the moment real authentication happens.
Configure sessionManagement().maximumSessions(1) in the SecurityFilterChain, which prevents a second concurrent login for the same user, either blocking the new login outright or expiring the older session, depending on how it's configured.
Clickjacking hides your site inside an invisible iframe on an attacker's page, tricking a user into clicking something they can't actually see. Spring Security sets the X-Frame-Options header to DENY by default, telling browsers to refuse rendering the page inside a frame at all, unless a specific application legitimately needs to be framed and opts out deliberately.
Telling a user specifically that the username exists but the password was wrong reveals which usernames are valid, letting an attacker enumerate real accounts before attempting to guess passwords against them. A generic message like invalid username or password, without specifying which one was wrong, avoids leaking that information.
Mid-Level (3-6 years)
Implement the interface's loadUserByUsername method, querying your user repository for a matching record, then mapping that entity into a UserDetails object (or having your own User entity implement UserDetails directly), throwing UsernameNotFoundException if no match is found. Register it as a bean, and Spring Security's default authentication flow picks it up automatically.
Implement the AuthenticationProvider interface's authenticate method with your own verification logic, whatever that needs to be, an external identity service, a legacy authentication system, then register it and Spring Security will delegate to it as one of the providers the AuthenticationManager tries during login.
DaoAuthenticationProvider is Spring Security's built-in provider for the standard username-password-against-a-UserDetailsService flow, and it's what gets used automatically in most setups without you writing a provider at all. A fully custom provider is only needed when authentication doesn't fit that standard shape, verifying against an external system with its own protocol, for example.
Implement a custom AuthenticationSuccessHandler and register it in the login configuration. Inside it, you have access to the Authentication object, including the user's granted authorities, letting you redirect an admin to one page and a regular user to another, rather than everyone landing on the same default page after login.
Implement a custom AuthenticationEntryPoint, which controls what happens when an unauthenticated request hits a protected endpoint, and register it in the exception handling configuration. Instead of the default redirect to an HTML login page, it can return a structured JSON error response with an appropriate status code, which is what an API client actually needs.
On successful login, the server generates and signs a JWT containing the user's identity and roles, returned to the client. On every subsequent request, the client sends that token in an Authorization header, and a custom filter validates its signature and expiry, then populates the SecurityContext with the authenticated user, all before the request reaches the controller.
Extend OncePerRequestFilter, extract the token from the Authorization header, verify its signature and expiration using your JWT library, and if valid, build an Authentication object and set it on the SecurityContext before calling the next filter in the chain. If the token is missing or invalid, you'd typically just let the request continue unauthenticated, letting the normal authorization rules reject it afterward.
Before UsernamePasswordAuthenticationFilter, using addFilterBefore(). Placing it there ensures the JWT gets validated and the SecurityContext populated before Spring Security's own authorization checks run, so a request with a valid token is correctly recognized as authenticated by the time those checks happen.
Issue a short-lived access token alongside a longer-lived refresh token at login. A dedicated endpoint accepts the refresh token and issues a new access token without requiring the user to log in again, and that refresh token itself needs to be stored securely and should be revocable server-side in case it's ever compromised.
A JWT, once issued, is valid until it actually expires. There's no built-in way to revoke it early, unlike a server-side session you can simply delete. Keeping the access token's lifetime short limits how long a stolen token stays useful to an attacker, while the separate refresh token handles keeping the user logged in without forcing them to re-enter credentials constantly.
@PreAuthorize checks its condition before the method runs, blocking the call entirely if it fails. @PostAuthorize checks its condition after the method has already executed, which lets you write an expression referencing the method's actual return value, useful when the authorization decision genuinely depends on data the method itself produces.
Combine them with a logical operator inside the SpEL expression, @PreAuthorize("hasRole('ADMIN') or #ownerId == authentication.principal.id"), letting either an admin or the resource's actual owner through, while denying everyone else.
@PostFilter automatically removes elements from a returned collection that don't satisfy the given security expression, applied once per element after the method returns. It centralizes that filtering logic declaratively at the method boundary instead of scattering manual filtering logic throughout the method body.
A long SpEL expression embedded directly in an annotation is hard to read, hard to test in isolation, and easy to get subtly wrong. For anything beyond a simple role check, a custom permission evaluator, referenced from a much shorter expression, keeps the actual logic in regular, testable Java code instead of buried inside a string.
OAuth2 is an authorization framework that lets a user grant a third-party application limited access to their data on another service, without ever sharing their actual password with that third party. Logging into an app using your Google account is a familiar example of OAuth2 in action.
OAuth2 is purely about authorization, granting access to a resource. OpenID Connect is built on top of OAuth2 and adds an actual identity layer, a standardized way to verify who the user is, typically through an ID token. In practice, most login-with-Google or login-with-Microsoft flows are technically using OpenID Connect, not plain OAuth2 alone.
Add the spring-boot-starter-oauth2-client dependency, configure the provider's client ID and secret in application.properties, and Spring Boot's auto-configuration wires up the entire login flow automatically, redirect to the provider, handle the callback, and populate an authenticated user, with very little custom code required.
A Resource Server is an application that hosts protected resources (an API) and validates incoming access tokens on each request, rather than handling the login flow itself. Spring Security's OAuth2 Resource Server support handles validating a JWT-based access token issued by a separate Authorization Server.
A stateful setup keeps a session on the server, tracking the logged-in user across requests via a session cookie, which is the traditional model for a browser-based application. A stateless setup keeps no server-side session at all, relying entirely on a token, usually a JWT, sent with every request, which fits a REST API better since it doesn't require sticky sessions or shared session storage across multiple server instances.
sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) tells Spring Security never to create or use an HTTP session, which is standard for a token-based REST API where the client, not the server, is responsible for carrying authentication state on every request.
Session hijacking is when an attacker steals a valid session identifier, often through a network sniffing attack or cross-site scripting, and uses it to impersonate the victim. Serving the application exclusively over HTTPS, marking session cookies as HttpOnly and Secure, and regenerating session IDs on login are the standard defenses, since each one closes off a different way the session ID could otherwise be exposed.
Spring Security's SessionRegistry keeps track of active sessions per user, and you can iterate through a specific user's registered sessions and call expireNow() on each one, forcing them to re-authenticate on their next request rather than continuing to use their old, now-outdated credentials.
JSESSIONID is the standard servlet container session cookie, tracking a server-side HTTP session automatically. A custom authentication cookie is something your application manages explicitly, often used to carry a JWT or another token in setups that intentionally avoid relying on standard server-side sessions at all.
Senior (6-8 years)
A PermissionEvaluator lets you plug custom, often domain-specific logic into Spring Security's expression language, used through hasPermission() in a @PreAuthorize expression. You'd build one when authorization depends on a genuinely complex relationship, like checking whether a user belongs to the same organization as the resource they're trying to access, logic that doesn't reduce cleanly to a simple role check.
A voter examines a request and votes to grant, deny, or abstain on access, and Spring Security combines the votes from every configured voter using a decision strategy, commonly requiring unanimous approval or just a simple majority, depending on configuration. This gives you a pluggable way to compose several independent authorization rules rather than cramming all the logic into one place.
ABAC bases a decision on multiple attributes at once, the user's department, the resource's sensitivity level, the time of day, rather than a single role check. In Spring Security, this typically means a custom PermissionEvaluator or a more elaborate SpEL expression that references several pieces of context together, rather than the simple hasRole() check RBAC usually relies on.
ACLs let you grant permissions on individual object instances, beyond a type of resource or a URL pattern. Role-based security can say a user with the EDITOR role can edit documents in general. ACLs can say this specific user can edit this specific document, which is exactly the fine-grained, per-object permission model a document-sharing or collaboration feature typically needs.
URL-based security rules don't help much here, since every request hits the same endpoint regardless of what data it's actually asking for. Authorization instead needs to happen at the resolver level, applying method security annotations on individual field or query resolvers, or building custom logic that inspects the incoming GraphQL query itself before deciding what to allow.
Add a custom filter positioned after authentication has already happened in the chain, so the authenticated user's identity is available, then track and enforce request counts keyed by that user ID rather than by IP, typically backed by Redis so the limit is enforced consistently across multiple application instances.
After a user's password is verified successfully, redirect them to a second-factor verification step, a TOTP code from an authenticator app, or an SMS code, before actually granting full authentication. This is typically implemented as a custom flow layered on top of Spring Security's standard authentication, since 2FA isn't part of the framework's core authentication process out of the box.
It lets a user stay logged in across browser sessions, even after closing and reopening the browser, without needing to log in again each time. Spring Security supports this through a signed cookie (simple, but somewhat less secure) or a persistent token stored in the database (more secure, and revocable server-side if needed), configured through rememberMe() in the SecurityFilterChain.
The simple hash-based approach encodes the username, an expiration time, and a hash into the cookie itself, with nothing stored server-side, so a leaked cookie remains valid until it naturally expires, with no way to revoke it early. The persistent token approach stores a token server-side that can be invalidated at any time, at the cost of needing a database table and slightly more implementation complexity.
Track failed attempts per user, typically in the database alongside the user record, and check that count during authentication, either in a custom AuthenticationProvider or by hooking into Spring Security's authentication failure and success event listeners to increment or reset the counter and lock the account once a threshold is crossed.
Spring Security publishes application events for things like successful logins, failed logins, and authorization denials, which you can listen for with a standard Spring @EventListener. A practical use is exactly the account lockout scenario above, incrementing a failed-attempt counter on an AuthenticationFailureBadCredentialsEvent, or logging every authorization denial for later security auditing.
A custom filter or an expression using hasIpAddress() within the authorization configuration restricts access based on the request's originating IP address, commonly used to lock down an admin panel to a known office network or VPN range. It's worth pairing with proper authentication rather than relying on IP restriction alone, since IP addresses can be spoofed or the restriction can become outdated as network setups change.
@WithMockUser lets you run a test as a specific authenticated user with defined roles, and combined with Spring's MockMvc, you can assert that a request to a protected endpoint succeeds for a user with the right role and returns a 403 for one without it. Testing the denied case explicitly matters just as much as testing the allowed one, since a security rule that only gets tested for the happy path can hide a misconfiguration that quietly grants access it shouldn't.
Lead (8-10 years)
The authorization code flow involves a real user logging in and granting consent, appropriate for a user-facing application. The client credentials flow has no user involved at all, used for machine-to-machine communication where one service authenticates directly as itself to call another service's API.
Spring Authorization Server is the current project for this, letting you issue and manage your own OAuth2 tokens rather than relying on Auth0, Okta, or a similar external provider. You'd need this specifically when you're building your own identity platform that other applications or services will authenticate against, rather than simply consuming someone else's login.
Scopes define what a specific access token is actually allowed to do, read-only access versus full write access, for example, and they're typically encoded directly as claims inside the JWT. In a Resource Server, you'd enforce them with an expression like hasAuthority('SCOPE_read'), checking that the token presented actually carries the required scope for the endpoint being called.
Configure the resource server with the identity provider's issuer URI, and Spring Security automatically fetches the provider's public signing keys from its JWKS endpoint to verify token signatures, without you needing to manage or hardcode those keys yourself. This is what most spring.security.oauth2.resourceserver configuration in application.properties is actually doing behind the scenes.
Introspection means calling back to the authorization server to ask whether a given token is still valid, rather than verifying a signature locally and trusting the token's own claims. It's necessary for opaque tokens that aren't JWTs at all, and it's also useful even with JWTs when you specifically need to know if a token has been revoked before its stated expiration.
Keep access token lifetimes short, so a compromised token has a naturally limited window of danger. For genuine early revocation, maintain a server-side denylist of specifically revoked token IDs that gets checked on validation, or fall back to token introspection against the authorization server instead of relying purely on local signature validation.
The ID token is meant to represent the authenticated user's identity, consumed directly by the client application to know who logged in. The access token is meant to be sent to a resource server to actually access protected data, and isn't intended to be parsed or trusted by the client application itself the way the ID token is.
PKCE adds a dynamically generated secret to the authorization code flow, preventing an intercepted authorization code from being exchanged for a token by anyone other than the client that originally initiated the request. It's particularly important for mobile and single-page applications, which can't securely store a traditional client secret the way a server-side application can, and it's now recommended for the authorization code flow generally, well beyond just those client types.
A central identity provider, whether Spring Authorization Server or an external one like Okta, issues tokens that every participating application trusts and validates independently as an OAuth2/OIDC Resource Server. A user logging into one application effectively gets recognized across all of them, since they all defer identity verification to that same central authority.
The gateway validates incoming tokens once, at the edge, and either forwards the request only if it's valid, or attaches verified identity information (like the user ID and roles as headers) for downstream services to trust. This avoids every single microservice needing its own full authentication logic, though downstream services usually still need lighter authorization checks of their own for anything specific to their own domain.
Centralizing at the gateway reduces duplicated logic and keeps token validation consistent everywhere. The trade-off is that a service is then trusting the gateway's word about who the caller is, which requires the network between the gateway and the internal services to actually be trusted and properly locked down, since a compromised or bypassed gateway could otherwise let unauthenticated traffic straight through.
A common pattern is the gateway forwarding the original validated JWT unchanged to downstream services, which each independently verify it themselves rather than blindly trusting a header the gateway claims to have already checked. This keeps every service capable of verifying identity on its own, rather than depending entirely on the gateway's word without any way to confirm it.
Separate SecurityFilterChain beans, matched to different URL patterns, let you apply genuinely different rules to each. The public API might require OAuth2 tokens with specific, narrower scopes and stricter rate limiting. The internal API might use a simpler, more permissive mechanism appropriate for trusted internal service-to-service traffic that doesn't need the same level of external scrutiny.
Never commit them to source control. Load them from environment variables or a dedicated secrets manager (Vault, AWS Secrets Manager) at runtime instead, referenced in application.properties through placeholder syntax rather than hardcoded values. For signing keys specifically, a proper key management service also handles the harder problem of key rotation over time, which hardcoded keys make painfully manual.
Staff (10+ years)
I'd weigh the real cost of building and maintaining identity infrastructure correctly, since getting authentication wrong has serious consequences, against the recurring cost and any genuine customization limits of a managed provider. For most organizations, a managed provider is the right call, since identity and authentication are rarely the actual differentiator worth investing scarce engineering time in building from scratch.
Migrate incrementally behind solid test coverage of the existing security rules first, since a security misconfiguration is exactly the kind of change that can go unnoticed until it's actually exploited, unlike an obvious bug that breaks something visibly right away. I'd stage the migration in a lower-risk environment and specifically test both the allowed and denied paths before rolling it out to production, since the happy path alone proves very little here.
I look specifically at what happens on the failure path, what an unauthenticated or unauthorized request actually gets back, since a misconfigured security rule often fails open rather than closed, quietly allowing access it shouldn't. I also check whether the change is consistent with authentication patterns already established elsewhere in the system, since an inconsistent one-off security pattern is a genuinely risky kind of technical debt to leave behind.
I'd push for a shared, well-tested internal library handling common security concerns, JWT validation, standard security headers, rather than letting each team reimplement authentication logic independently with its own subtle inconsistencies. Centralizing the parts that genuinely benefit from consistency, while leaving service-specific authorization logic to each team, tends to hold up better than a long security policy document nobody actually reads.
I'd read the migration guide closely for behavioral changes specifically, beyond just API changes, since a security library can quietly change how something is enforced without breaking compilation at all. I'd pilot the upgrade on a lower-risk internal service first, with genuinely thorough authorization testing, before rolling it out to anything customer-facing or handling sensitive data.
First check whether it's actually a security bug or a symptom of something else, like a load balancer misconfiguration causing session affinity to break in a stateful setup, or clock drift between servers causing JWT expiry checks to fail inconsistently. Security-adjacent problems often turn out to be infrastructure problems wearing a security-shaped costume.
Track failed login rates and authorization denial rates over time, and alert on unusual spikes specifically, since a sudden spike in failed logins from a narrow set of IPs is a strong signal of a credential-stuffing attack in progress, distinct from normal background noise. I'd also make sure these security events are logged with enough context, timestamp, IP, endpoint, to actually investigate an incident after the fact rather than just knowing something happened.
Treat the token format and claims structure as a contract with every consuming service. Adding a new claim is generally safe. Changing or removing an existing one needs a deprecation period and direct coordination with every team consuming it, since a silent change can break authorization checks in a service you don't even directly own or have visibility into.
Contain first: fix or disable the specific misconfigured endpoint immediately, even before fully understanding the complete scope of the exposure. Then assess actual impact, what data was exposed, to whom, for how long, since that scope genuinely determines what happens next, including whether the incident needs to be disclosed. I'd also want a root-cause review afterward specifically asking how this passed review and testing in the first place, since patching this one instance alone leaves the door open for the next one.
I'd want both automated scanning, for known common vulnerabilities and misconfigurations, and manual testing specifically targeting the application's actual business logic and authorization boundaries, since automated tools reliably catch the generic issues but consistently miss a subtle, business-specific authorization bypass that only makes sense in the context of what the application actually does.
This is a judgment question interviewers use to see how you reason under uncertainty, not to test a specific fact. A strong answer names the actual constraint that forced the decision, the realistic options that were genuinely on the table, why you picked one knowing it wasn't guaranteed to be right, and what you'd do differently with what you know now, since security decisions in particular often get made with an incomplete picture of the actual threat model.
I'd walk through a real security review together, specifically asking what happens if this check fails, or if this token is malformed, or if this role is missing, for each piece of their configuration, rather than lecturing about defensive thinking in the abstract. Seeing their own configuration through an attacker's eyes, even briefly, tends to change how carefully they write the next security rule.
I wouldn't lead with security as an abstract principle. I'd point to a realistic, concrete scenario specific to their actual application, what happens if this exact endpoint gets hit by a credential-stuffing attack, or what a specific misconfiguration would actually expose, and let that concrete picture make the case rather than a general argument about security being important.
I'd try to find the actual middle ground rather than treating it as security against user experience. Often there's a way to get most of the security benefit with meaningfully less friction, a smarter session timeout instead of an aggressive one, or step-up authentication only for genuinely sensitive actions rather than applying the strictest check everywhere. Bringing options rather than a single non-negotiable requirement tends to resolve this faster than holding a firm line.
I'd translate the risk into terms leadership already tracks: the cost and reputational damage of a realistic breach scenario given the current gaps, compliance requirements the current setup might not actually satisfy, and engineering hours already being spent working around the current system's limitations. Framed as risk reduction and cost avoidance with a concrete scenario attached, it competes far better for budget than framed as a technical improvement for its own sake.




