Spring Security - Complete Notes

A practical and interview-focused guide to authentication, authorization, the servlet security filter chain, sessions, passwords, JWTs, OAuth 2.0, OpenID Connect, method security, browser protections, testing, and production hardening.

00. The security mental model

Spring Security is a framework for deciding who a caller is, what that caller may do, and how an application resists common attacks while making those decisions.

Security is not one login page. It is a chain of controls around every protected operation. A request may need transport security, credential extraction, credential validation, identity persistence, authorization, safe error handling, audit events, and browser protections. A defect in any layer can defeat correct code in another layer.

Think of an airport. Authentication checks that the passport belongs to the traveler. Authorization checks whether the boarding pass permits entry to this flight and gate. Security controls also protect the building, luggage, records, and staff workflows. A valid passport does not grant access everywhere.

Concept Question Typical Spring representation
Authentication Who or what is calling? Authentication, AuthenticationManager
Authorization May this identity perform this action on this resource? AuthorizationManager, request and method rules
Credential What evidence supports the identity? Password, session cookie, bearer token, client certificate
Principal Whose identity was established? UserDetails, OIDC user, JWT subject, custom object
Authority What named capability or role was granted? GrantedAuthority
Deny by default

Start with every request and operation denied, then explicitly permit the smallest required surface. Authentication alone is never an authorization policy. Validate ownership, tenant, state, amount, and other business facts at the service boundary.

01. Spring Boot starting point

Spring Boot supplies dependency management and safe defaults, while the application owns its actual access policy.

Adding spring-boot-starter-security makes a servlet web application secure by default. Boot creates a generated development password and protects requests when the application has not supplied its own web-security configuration. Real applications should declare a SecurityFilterChain bean and identity infrastructure appropriate to their trust model. Defining the bean causes Boot's default web policy to back off.

A clear session-based policy
@Configuration
@EnableWebSecurity
@EnableMethodSecurity
public class SecurityConfig {

    @Bean
    SecurityFilterChain applicationSecurity(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/", "/assets/**", "/login").permitAll()
                .requestMatchers(HttpMethod.GET, "/api/catalog/**").permitAll()
                .requestMatchers("/api/admin/**").hasRole("ADMIN")
                .anyRequest().authenticated())
            .formLogin(form -> form
                .loginPage("/login")
                .permitAll())
            .logout(logout -> logout
                .logoutSuccessUrl("/?logout"))
            .sessionManagement(session -> session
                .sessionFixation(fixation -> fixation.changeSessionId()));

        return http.build();
    }

    @Bean
    PasswordEncoder passwordEncoder() {
        return PasswordEncoderFactories.createDelegatingPasswordEncoder();
    }
}

Match specific routes before anyRequest(). Rules are evaluated in declaration order, and the first matching authorization rule decides. A broad permitAll() placed too early can expose every route below it. Static resources may be permitted through the chain so Spring Security can still write headers; completely ignoring them bypasses all security filters.

02. Security filter chain architecture

Servlet security runs mainly before controllers, through filters assembled and ordered by Spring Security.

The servlet container delegates to DelegatingFilterProxy, which looks up the Spring bean named springSecurityFilterChain. That bean is normally a FilterChainProxy. It chooses the first matching SecurityFilterChain and invokes that chain's security filters in a defined order.

Stage Representative filter or component Purpose
Load context SecurityContextHolderFilter Makes the stored authentication available for the request.
Browser defenses HeaderWriterFilter, CsrfFilter Writes security headers and validates CSRF tokens.
Logout LogoutFilter Clears authentication and configured client state.
Authentication Form, Basic, bearer, or custom authentication filters Extracts a credential and attempts authentication.
Error translation ExceptionTranslationFilter Turns security exceptions into login, 401, or 403 responses.
Request authorization AuthorizationFilter Applies the matching endpoint rule before application code.

Filter order is part of the security design. Authentication must happen before authorization. Exception translation must surround later filters that can deny access. Prefer built-in DSL hooks. If a custom filter is unavoidable, use addFilterBefore, addFilterAfter, or addFilterAt relative to a known filter and document why that location is correct.

Multiple filter chains

Separate chains are useful when, for example, /api/** uses bearer tokens and browser pages use sessions. Give each chain a securityMatcher and an explicit @Order. The first chain whose matcher accepts the request wins. Include a final catch-all chain, or unmatched requests receive no Spring Security protection.

Independent API and browser chains
@Bean
@Order(1)
SecurityFilterChain api(HttpSecurity http) throws Exception {
    http
        .securityMatcher("/api/**")
        .authorizeHttpRequests(auth -> auth.anyRequest().authenticated())
        .oauth2ResourceServer(oauth -> oauth.jwt(Customizer.withDefaults()))
        .sessionManagement(session ->
            session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
        .csrf(AbstractHttpConfigurer::disable);
    return http.build();
}

@Bean
@Order(2)
SecurityFilterChain browser(HttpSecurity http) throws Exception {
    http
        .authorizeHttpRequests(auth -> auth
            .requestMatchers("/", "/assets/**").permitAll()
            .anyRequest().authenticated())
        .formLogin(Customizer.withDefaults());
    return http.build();
}

03. Authentication internals

Authentication converts untrusted credential input into a trusted application identity.

An authentication filter creates an unauthenticated Authentication token and sends it to an AuthenticationManager. The common ProviderManager tries compatible AuthenticationProvider instances. A username/password provider may load UserDetails through a UserDetailsService and compare the submitted password with a PasswordEncoder. A bearer provider instead validates a JWT or introspects an opaque token. Successful authentication returns an authenticated object containing a principal and authorities.

Trust boundary

Never set authenticated=true merely because a request contains a username or token. Only a trusted provider should produce an authenticated result after verification. Erase raw credentials after authentication and never log them.

UserDetailsService loads account facts. It does not itself verify the submitted password. Account locked, disabled, expired, and credential-expired flags are separate checks. Return generic failures such as "invalid credentials" to clients so an attacker cannot enumerate usernames, while recording safe diagnostic detail internally.

04. SecurityContext and identity propagation

The SecurityContext holds the current Authentication for the execution.

Imperative servlet applications normally access it through SecurityContextHolder, backed by thread-local storage. Spring's filters load the context before application work and clear it after the request. Clearing is essential because servlet-container threads are reused. Prefer method arguments such as @AuthenticationPrincipal or an injected facade over scattering static context access throughout domain code.

Read the established principal at the web boundary
@GetMapping("/api/me")
UserProfile me(@AuthenticationPrincipal(expression = "subject") String subject) {
    return profiles.findBySubject(subject);
}

A thread local does not automatically cross to @Async, a custom executor, a new thread, or a messaging consumer. Use Spring's security-context-aware executors where appropriate, or explicitly pass the identity facts needed by the operation. Do not blindly propagate an entire user context into background jobs. For Reactor applications, identity lives in Reactor Context, not a thread local.

05. Password storage and login defense

Store a slow, salted, one-way password hash, never plaintext and never reversible encryption.

Spring Security's DelegatingPasswordEncoder uses a storage prefix such as {bcrypt} to select an encoder. This permits old hashes to remain verifiable while new passwords use the current algorithm. Adaptive algorithms such as bcrypt, PBKDF2, scrypt, and Argon2 deliberately consume work. Tune the cost on production-class hardware so verification is expensive to attack but acceptable for real login traffic.

Encoding and transparent upgrade
PasswordEncoder encoder =
        PasswordEncoderFactories.createDelegatingPasswordEncoder();

String stored = encoder.encode(rawPassword);

if (encoder.matches(loginPassword, stored)
        && encoder.upgradeEncoding(stored)) {
    users.replacePasswordHash(userId, encoder.encode(loginPassword));
}
  • Generate no application-level salt for encoders that already create a random salt.
  • Never compare encoded strings directly because a random salt changes the result.
  • Reject {noop} and plaintext outside tightly isolated demonstrations.
  • Rate-limit by several signals, add increasing delay, and alert on credential stuffing.
  • Use MFA or passkeys for sensitive accounts and step-up authentication for dangerous actions.
  • Make reset tokens random, single-use, short-lived, securely stored, and invalid after use.
  • Do not log passwords, reset links, session IDs, authorization codes, or bearer tokens.

A very high hash cost can exhaust the server under attack. Isolate password verification capacity, apply rate limits before expensive work where safe, and monitor latency and rejection counts. Error responses should not reveal whether a username, email, or tenant exists.

06. Authorization, roles, and authorities

Good authorization models capabilities and resource relationships, not only job titles.

A GrantedAuthority is a string understood by policy, such as invoice:read. hasRole("ADMIN") normally checks for ROLE_ADMIN; hasAuthority("ADMIN") checks the exact string. Mixing these conventions causes accidental denials or grants. Pick one documented vocabulary.

Model Example Strength and limitation
RBAC Role BILLING_ADMIN Easy to understand, but broad roles grow quickly.
Permission invoice:approve Precise and reusable, but needs lifecycle governance.
Relationship User owns order 42 Protects individual resources and prevents IDOR.
Attribute-based Tenant, region, amount, time, device risk Expressive, but policy needs careful testing and explanation.

Request rules protect routes. Service-level rules protect business operations even when reached from a controller, message listener, scheduled task, or another service. Object-level checks stop insecure direct object reference attacks: a caller who may read orders in general must still be allowed to read this order. Query data with tenant and ownership predicates, then enforce again at the service boundary for sensitive changes.

07. Method security

@EnableMethodSecurity activates authorization around Spring-managed method calls.

Capabilities and object ownership at the service layer
@Service
public class InvoiceService {

    @PreAuthorize("hasAuthority('invoice:read') and "
            + "@invoiceAccess.canRead(#invoiceId, authentication)")
    public InvoiceView find(UUID invoiceId) {
        return repository.findView(invoiceId)
                .orElseThrow(InvoiceNotFoundException::new);
    }

    @PreAuthorize("hasAuthority('invoice:approve')")
    public void approve(UUID invoiceId) {
        // Enforce business state and tenant constraints here too.
    }
}

@PreAuthorize checks before invocation. @PostAuthorize can inspect a returned object, but work has already run and data has already been loaded, so prefer pre-checks when possible. @PreFilter and @PostFilter may be convenient for small collections but can load data that should have been filtered in the database.

Method security uses Spring AOP. A call from one method to another method on the same instance can bypass the proxy, as can manually constructed objects. Put protected operations on a Spring-managed service reached through its proxy. Be deliberate about advice ordering when @Transactional and authorization share a method, especially if authorization queries the database.

08. Stateful session security

In session-based security, the browser holds an opaque session cookie and the server holds the authenticated state.

After login, Spring stores the SecurityContext through a SecurityContextRepository, commonly in HttpSession. On later requests the browser sends the session cookie, the server resolves the session, and the security filters restore authentication. This makes immediate server-side revocation and logout natural, at the cost of shared session storage or sticky routing in a cluster.

  • Use cookies with Secure, HttpOnly, and an appropriate SameSite value.
  • Rotate or change the session ID after authentication to resist session fixation.
  • Define idle and absolute timeouts according to risk, and require reauthentication for critical actions.
  • Consider concurrent-session limits for privileged users, but understand clustered registry requirements.
  • Do not place sensitive domain objects or unbounded data in the session.
  • Invalidate server state on logout and credential-risk events.

Session cookies are credentials. CSRF matters because a browser attaches them automatically. Horizontal scaling needs a shared store such as Spring Session with Redis or a load-balancer design whose failure behavior is understood. Monitor session count, store latency, expiration, and serialization compatibility during deployments.

09. Stateless APIs

Stateless security means each request carries enough credential material to authenticate without a server login session.

OAuth 2.0 resource server policy
@Bean
SecurityFilterChain apiSecurity(HttpSecurity http) throws Exception {
    http
        .authorizeHttpRequests(auth -> auth
            .requestMatchers(HttpMethod.GET, "/api/public/**").permitAll()
            .requestMatchers("/api/admin/**").hasAuthority("SCOPE_admin")
            .anyRequest().authenticated())
        .oauth2ResourceServer(oauth -> oauth.jwt(Customizer.withDefaults()))
        .sessionManagement(session ->
            session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
        .requestCache(RequestCacheConfigurer::disable)
        .csrf(AbstractHttpConfigurer::disable);
    return http.build();
}

Do not call an API stateless merely because it uses a JWT. If a filter stores authentication in an HTTP session, or the API accepts an automatically attached authentication cookie, state and CSRF concerns remain. Disabling CSRF is reasonable for an API that accepts bearer tokens only in the Authorization header and never authenticates by cookie. It is unsafe as a blanket copy-and-paste rule.

10. JWT access tokens

A JWT is a token format, not an authentication protocol and not automatically a secure design.

A signed JWT has encoded header, claims, and signature parts. Encoding does not hide claims. Anyone holding the token can usually read them. The resource server must verify the signature with an explicitly allowed algorithm and trusted key, then validate claims required by the deployment.

Claim Meaning Required check or use
iss Issuer Must equal the configured trusted issuer.
sub Subject identifier Use a stable identifier, not display text.
aud Intended recipient Reject a token not intended for this API.
exp Expiration time Reject expired tokens with small, controlled clock skew.
nbf Not valid before Reject premature use.
iat Issued at Can enforce maximum age and aid investigation.
jti Unique token identifier Useful for tracing or a targeted denylist.
Boot resource-server configuration
spring:
  security:
    oauth2:
      resourceserver:
        jwt:
          issuer-uri: https://identity.example.com
          audiences:
            - https://orders.example.com

With issuer discovery, Spring Security obtains key metadata, validates signature and timestamps, validates the issuer, and maps scopes to authorities prefixed with SCOPE_ by default. Configure audience validation as well. Plan key rotation with overlapping old and new verification keys, stable kid values, bounded caches, alerting, and an emergency revocation path.

JWT failure modes

Never accept alg=none, derive the accepted algorithm from an untrusted token, trust an arbitrary key URL from a header, put secrets or excessive personal data in claims, use an ID token as an API access token, or accept access tokens for the wrong audience. Short token lifetime limits damage but does not prevent theft.

11. Access and refresh tokens

Access tokens call resource servers. Refresh tokens obtain new access tokens from the authorization server and usually deserve stronger protection.

Keep access tokens short-lived and narrowly scoped. Store refresh tokens only where the client type can protect them. For public clients, current OAuth security best practice requires refresh tokens to be sender-constrained or use refresh-token rotation. With rotation, every successful refresh invalidates the old token and returns a new one. Reuse of an older member signals theft; revoke the active token family and require authentication again.

  • Store an opaque refresh token as a strong hash when lookup design permits.
  • Bind it to the client, user, grant, device context where appropriate, and authorized scopes.
  • Apply idle and absolute expiry, revoke on logout or compromise, and keep an audit trail.
  • Perform rotation atomically to prevent two refresh requests both succeeding.
  • Never send bearer tokens in URLs because logs, history, referrers, and monitoring can leak them.
  • Avoid persistent browser storage for high-value bearer tokens when a backend-for-frontend can hold tokens and issue a hardened session cookie instead.

Self-contained JWT access tokens are hard to revoke instantly. Options include very short lifetimes, token-version checks, a denylist keyed by jti, opaque-token introspection, or gateway enforcement. Every option trades availability, latency, storage, and revocation speed.

12. OAuth 2.0 fundamentals

OAuth 2.0 delegates authorization: a client receives limited access to a protected resource without receiving the user's password.

Role Responsibility
Resource owner Entity able to grant access, commonly the end user.
Client Application requesting delegated access.
Authorization server Authenticates as needed, obtains authorization, and issues tokens.
Resource server API that validates access tokens and enforces scope plus business policy.

Prefer authorization code flow with PKCE for user-facing clients. The client creates a random code_verifier, sends its SHA-256-derived code_challenge in the authorization request, then proves possession by sending the verifier to the token endpoint. PKCE prevents a stolen authorization code from being redeemed without its verifier. Use exact redirect-URI matching, state to bind the browser response to the initiating flow, and TLS everywhere.

Client credentials is for a confidential machine acting as itself, not for impersonating an end user. Do not use the resource-owner password grant or the implicit grant in new systems. Restrict tokens by audience and least-privilege scopes. Consent is a user experience and governance control, not a substitute for resource-server authorization.

13. OpenID Connect and login flows

OpenID Connect adds an interoperable identity layer to OAuth 2.0.

An OIDC authentication request includes the openid scope. The client receives an ID token whose claims describe the authentication and subject. An access token is for an API. An ID token is evidence intended for the client. Validate ID-token signature, issuer, audience, time claims, authorized party where applicable, and nonce when used. Never send an ID token to an unrelated API as an access token.

  1. The application starts authorization code flow and records protected state, nonce, and PKCE verifier values.
  2. The browser authenticates at the OpenID Provider and grants requested access.
  3. The provider redirects with a short-lived authorization code and the original state.
  4. The client verifies state and exchanges the code plus PKCE verifier at the token endpoint.
  5. The client validates the ID token, establishes its local identity or session, and stores tokens according to the client threat model.
Boot OIDC client registration
spring:
  security:
    oauth2:
      client:
        registration:
          corporate:
            provider: corporate-idp
            client-id: ${OIDC_CLIENT_ID}
            client-secret: ${OIDC_CLIENT_SECRET}
            authorization-grant-type: authorization_code
            scope: openid,profile,email
        provider:
          corporate-idp:
            issuer-uri: https://identity.example.com

Keep the client secret in a secret manager, not source control. Provider discovery reduces copied endpoint configuration but makes issuer trust especially important. Decide how external subjects map to local accounts, handle renamed email safely, and key identity by issuer plus subject rather than email alone.

14. Resource servers and claim mapping

A resource server authenticates the token, then independently authorizes the requested operation.

Spring Security supports JWT validation and opaque-token introspection. JWT validation is local after keys are available, which improves latency and authorization-server independence. Introspection asks an authorization server about token activity, enabling centralized revocation but adding network latency and availability coupling. Cache only within the risk tolerance of delayed revocation.

Map a custom permissions claim
@Bean
JwtAuthenticationConverter jwtAuthenticationConverter() {
    JwtGrantedAuthoritiesConverter permissions =
            new JwtGrantedAuthoritiesConverter();
    permissions.setAuthoritiesClaimName("permissions");
    permissions.setAuthorityPrefix("");

    JwtAuthenticationConverter converter =
            new JwtAuthenticationConverter();
    converter.setJwtGrantedAuthoritiesConverter(permissions);
    converter.setPrincipalClaimName("sub");
    return converter;
}

// In HttpSecurity:
// .oauth2ResourceServer(oauth -> oauth.jwt(jwt ->
//     jwt.jwtAuthenticationConverter(jwtAuthenticationConverter())))

Treat claims as authorization input issued by a particular trusted authority. Keep token size bounded because every request carries it. Avoid embedding rapidly changing permissions unless the lifetime is correspondingly short. Multi-issuer systems must select a trusted decoder by request context without letting an attacker introduce arbitrary issuers.

15. CSRF

Cross-Site Request Forgery makes a victim's browser send an unwanted state-changing request using credentials the browser attaches automatically.

Spring Security enables CSRF protection by default for unsafe methods. The server creates an unpredictable token bound to the user's context, and the legitimate page sends that value in a form field or request header. An attacking site can cause a request but normally cannot read the token because of browser origin controls.

SPA token repository when a cookie-readable token is required
http.csrf(csrf -> csrf
    .csrfTokenRepository(
        CookieCsrfTokenRepository.withHttpOnlyFalse()));

Making the CSRF token cookie readable is intentional only when JavaScript must copy it into a header. The authentication cookie should remain HttpOnly. Account for Spring Security's deferred token and BREACH-protection behavior when building an SPA. Login and logout also need CSRF protection. Use POST logout with a valid token, not a state-changing GET.

When disabling CSRF is wrong

JSON does not prevent CSRF. If the browser authenticates with a session cookie, Basic credentials, a client certificate, or any automatically attached credential, evaluate CSRF. Disable it only for a chain whose authentication mechanism and clients make the attack inapplicable.

16. CORS

CORS tells browsers which origins may read or send selected cross-origin requests. It is not authentication and does not protect non-browser clients.

Preflight requests normally have no session cookie, so CORS must run before authentication. Provide a UrlBasedCorsConfigurationSource and enable http.cors(). Allow exact trusted origins, methods, and headers. If credentials are allowed, never use a wildcard origin. Keep production origins out of client-controlled input.

Explicit CORS policy
@Bean
UrlBasedCorsConfigurationSource corsConfigurationSource() {
    CorsConfiguration cors = new CorsConfiguration();
    cors.setAllowedOrigins(List.of("https://app.example.com"));
    cors.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE"));
    cors.setAllowedHeaders(List.of("Authorization", "Content-Type", "X-CSRF-TOKEN"));
    cors.setAllowCredentials(true);
    cors.setMaxAge(Duration.ofHours(1));

    UrlBasedCorsConfigurationSource source =
            new UrlBasedCorsConfigurationSource();
    source.registerCorsConfiguration("/api/**", cors);
    return source;
}

17. Security response headers and transport

Spring Security writes useful browser-security headers by default, but each deployment still needs an explicit HTTPS and content policy.

  • Strict-Transport-Security tells browsers to use HTTPS for future requests and is written only on secure requests.
  • X-Content-Type-Options: nosniff reduces content-type confusion.
  • X-Frame-Options or CSP frame-ancestors controls framing and clickjacking risk.
  • Cache-control defaults prevent sensitive authenticated responses from being stored.
  • A carefully deployed Content Security Policy reduces XSS impact and controls resource origins.
  • Referrer-Policy and Permissions-Policy limit information and browser capabilities where suitable.

Terminate TLS only at trusted infrastructure. Configure forwarded-header handling only for trusted proxies so redirects and secure-cookie decisions see the original scheme without trusting forged client headers. Test HSTS, cookie flags, CSP, and redirects in the real proxy path. Do not disable all default headers to fix one embedding or caching issue; customize the specific policy.

18. Security exception handling

Authentication failure and insufficient authorization are different outcomes.

Situation HTTP result Spring hook
No valid authentication for a protected API 401 Unauthorized, often with WWW-Authenticate AuthenticationEntryPoint
Authenticated caller lacks permission 403 Forbidden AccessDeniedHandler
Browser needs interactive login Redirect to login or authorization endpoint Login entry point

Return a stable, sanitized JSON error shape for APIs and a correlation ID that operators can use. Do not reveal token claims, expected signatures, account existence, policy internals, or stack traces. Log enough structured context to investigate, but redact credentials and personal data. A missing resource may deliberately return 404 rather than reveal its existence to an unauthorized caller, provided the policy is consistent.

19. Logout and revocation

Logout must clear every relevant local credential, while federated logout may also involve the identity provider.

Spring Security's default servlet logout invalidates the HTTP session, clears the security context and repository, removes remember-me state, clears saved CSRF state, and publishes a logout event. With CSRF enabled, POST to /logout with the token. Consider the Clear-Site-Data header for cookies, storage, or cache where its broad browser effect is desired.

Deleting a JWT in one browser does not revoke copies. Revoke refresh tokens and decide whether access-token denylisting or short expiry meets the risk. OIDC local logout does not necessarily end the provider session, and provider logout does not automatically clear every relying-party session. Implement supported RP-initiated or back-channel logout when the product requires true single logout.

20. Common vulnerabilities and mistakes

Mistake Why it fails Safer approach
Only hiding buttons in the UI Attackers call the endpoint directly. Authorize on the server at request and service boundaries.
Checking only that a user is logged in Any user can access another user's object. Check permission, tenant, ownership, and object state.
Broad matcher before specific rules First-match behavior can expose or block later paths. Order specific matchers first and test negative cases.
Disabling CSRF because the API uses JSON Browsers still attach cookies automatically. Base the decision on credential transport, not media type.
Wildcard credentialed CORS Untrusted sites may act with browser credentials. Allow a small exact origin list.
Long-lived bearer tokens in logs or browser storage Any holder can replay a stolen token. Short lifetime, safe storage, redaction, rotation, sender constraint.
Trusting JWT signature alone A valid token may have the wrong issuer, audience, or time window. Validate algorithm, key, issuer, audience, time, and required claims.
Self-invoking a secured method The call can bypass the AOP proxy. Call a separate secured Spring bean through its proxy.
Writing a custom authentication filter unnecessarily Ordering, persistence, failure, and context cleanup are easy to get wrong. Compose built-in authentication mechanisms and providers.
Returning detailed login errors Enables account discovery and targeted attacks. Use generic client errors and safe internal diagnostics.

Spring Security does not replace output encoding, parameterized SQL, file validation, secure deserialization, dependency patching, SSRF controls, business fraud checks, secret management, or infrastructure hardening. Use the OWASP threat categories to review the whole system, not merely the security configuration class.

21. Testing security

Security tests must prove denial as carefully as success.

Add spring-security-test in test scope. With MockMvc, apply the Spring Security configurer when setup is manual. Use @WithMockUser for simple method tests, user() for request principals, jwt() for resource-server claims, and csrf() for state-changing browser requests. Keep tests that exercise the real decoder and identity provider contract at a separate integration level.

Positive and negative MockMvc tests
@Test
void adminEndpointRejectsAnonymousUser() throws Exception {
    mvc.perform(get("/api/admin/report"))
        .andExpect(status().isUnauthorized());
}

@Test
void adminEndpointRejectsTokenWithoutScope() throws Exception {
    mvc.perform(get("/api/admin/report")
            .with(jwt().authorities(new SimpleGrantedAuthority("SCOPE_read"))))
        .andExpect(status().isForbidden());
}

@Test
void updateRequiresCsrfForSessionUser() throws Exception {
    mvc.perform(put("/profile")
            .with(user("sam"))
            .contentType(MediaType.APPLICATION_JSON)
            .content("{\"displayName\":\"Sam\"}"))
        .andExpect(status().isForbidden());

    mvc.perform(put("/profile")
            .with(user("sam"))
            .with(csrf())
            .contentType(MediaType.APPLICATION_JSON)
            .content("{\"displayName\":\"Sam\"}"))
        .andExpect(status().isOk());
}
  • Test anonymous, authenticated-but-forbidden, and allowed users for every sensitive policy.
  • Test role-prefix, scope, claim, tenant, and ownership mapping with realistic principal types.
  • Test expired, wrong-issuer, wrong-audience, malformed, and incorrectly signed tokens.
  • Test CORS preflight, CSRF missing and valid tokens, logout, session fixation, and headers.
  • Test matcher boundaries such as trailing slashes, alternate HTTP methods, encoded paths, error dispatches, and management endpoints.
  • Test custom failure JSON without asserting secrets or unstable internal messages.

Mock authentication skips credential verification, so it cannot prove that a production decoder or password flow is configured correctly. Maintain a smaller number of full integration tests using generated signing keys, a controlled authorization server, or the real provider's supported test environment.

22. Performance and reliability

Security work consumes CPU, network, storage, and latency budgets, and attackers intentionally target those costs.

  • Password hashing is CPU or memory intensive. Tune it, rate-limit it, and capacity-plan peak login and reset traffic.
  • JWT verification is local but key discovery and rotation need bounded, observable caching.
  • Opaque introspection adds a remote call. Configure short timeouts, connection pools, failure behavior, and cautious caching.
  • Method security that queries a database can create repeated authorization queries. Batch, cache only safe facts, or combine access predicates with the data query.
  • Large JWTs increase request bandwidth and can exceed proxy or header limits.
  • Session stores become critical dependencies. Monitor saturation, eviction, replication, and outage behavior.

Do not fail open because an authorization dependency is slow. Decide which public endpoints can continue independently and deny protected operations when identity or policy cannot be validated. Separate availability alerts from security alerts so operators understand whether a spike is attack traffic, a provider outage, or a deployment defect.

23. Production security checklist

  1. Write a threat model listing assets, actors, trust boundaries, entry points, and abuse cases.
  2. Use supported Spring Boot and Spring Security releases and patch dependencies continuously.
  3. Require TLS, validate proxy trust, secure cookies, and deploy intentional security headers.
  4. Deny by default, use least privilege, and enforce tenant and object-level access.
  5. Keep client secrets, signing keys, passwords, and tokens in managed secret systems with rotation and access auditing.
  6. Validate OAuth issuer, audience, redirect URI, state, nonce, PKCE, scope, and token lifetime.
  7. Use MFA for privileged identities and isolate administrative endpoints and credentials.
  8. Rate-limit login, reset, token, and high-risk business actions without creating easy denial of service against one victim.
  9. Record authentication, authorization denial, privilege change, token reuse, and administrative events with redaction and useful correlation.
  10. Alert on unusual failures, impossible travel where appropriate, privilege escalation, key errors, refresh reuse, and configuration drift.
  11. Prepare key compromise, credential stuffing, session theft, and provider outage runbooks. Exercise revocation and recovery before an incident.
  12. Run code review, dependency scanning, secret scanning, SAST, DAST, penetration testing, and policy regression tests in proportion to risk.
Audit logs are not debug logs

An audit record should answer who attempted what, on which protected resource, when, from which relevant context, and with what outcome. It should never contain passwords, bearer tokens, session IDs, authorization codes, or unnecessary personal data.

24. Practice exercises

  1. Build a session-based notes application. Require CSRF for writes, rotate the session ID at login, secure cookies, and test logout plus anonymous denial.
  2. Protect an orders API as a JWT resource server. Validate issuer and audience, map orders.read and orders.write, and reject wrong claims.
  3. Add tenant-aware method security. Ensure a support role can read only assigned tenants and an ordinary user can read only owned orders.
  4. Model refresh-token families with atomic rotation. Simulate reuse of an old token and revoke the family.
  5. Configure OIDC login with authorization code and PKCE. Explain state, nonce, redirect URI, ID token, and access token validation to a reviewer.
  6. Write a security test matrix for every endpoint: anonymous, insufficient permission, wrong tenant, correct permission, invalid CSRF, wrong HTTP method, and malformed token.
  7. Inspect a deployed response through its real proxy. Verify HTTPS redirects, HSTS, CSP, clickjacking policy, cache headers, cookie flags, and redaction.

25. Interview questions and answers

What is the difference between authentication and authorization?

Authentication establishes an identity from credentials. Authorization decides whether that established identity may perform a specific action on a specific resource. Authorization follows authentication but may also allow anonymous operations.

How does Spring Security protect a servlet request?

The servlet container invokes DelegatingFilterProxy, which delegates to Spring's FilterChainProxy. It selects the first matching SecurityFilterChain. Ordered filters load context, apply exploit protections, authenticate, translate security errors, authorize the request, and clear context after processing.

What is inside Authentication?

It normally contains the principal, credentials, granted authorities, request details, and an authenticated flag. Before verification it represents an authentication request. After a trusted provider succeeds, it represents the established identity. Credentials should then be erased when possible.

Why is PasswordEncoder one-way?

The server needs to compare a submitted password, not recover stored passwords. Adaptive salted hashes reduce the value of a stolen database and slow offline guessing. Reversible encryption exposes every password if the decryption key is stolen.

What is the difference between hasRole and hasAuthority?

hasAuthority("x") checks the exact authority. hasRole("ADMIN") normally checks ROLE_ADMIN. Teams should use a consistent convention to prevent prefix bugs.

Why can method security be bypassed by self-invocation?

Default method security is proxy based. A call through this stays inside the target and never enters the proxy interceptor. Move the protected operation to another Spring bean or otherwise ensure calls pass through the proxy.

When should CSRF be disabled?

Only when the chain cannot authenticate through credentials a browser attaches automatically, such as a bearer-only API receiving the token in an authorization header. Session-cookie browser applications should normally retain CSRF protection, regardless of whether their body is JSON.

What is the difference between CORS and CSRF?

CORS controls whether a browser lets one origin interact with another origin. CSRF prevents an attacker from causing a victim's browser to perform an unwanted authenticated action. CORS is not an authorization mechanism, and a CORS policy does not by itself stop CSRF.

Session or JWT: which is better?

Neither is universally better. Sessions provide small opaque cookies and immediate server-side invalidation, but need shared state when scaled. JWT access tokens enable local validation across services, but increase token exposure and make immediate revocation harder. Choose from trust boundaries, clients, revocation needs, latency, and operational capacity.

What must a resource server validate in a JWT?

It must use a trusted key and allowed algorithm, then validate issuer, intended audience, expiration, not-before time, and every application-required claim. It must also safely map scopes or permissions and still enforce resource-level business policy.

What is the difference between an access token and an ID token?

An access token is presented to a resource server to authorize API access. An OIDC ID token gives the client information about an authentication event and subject. The client validates it. APIs should not accept an ID token in place of an access token.

Why use PKCE?

PKCE binds an authorization request to the later token exchange. A client sends a challenge derived from a secret verifier and must present that verifier to redeem the code. A stolen authorization code alone is therefore insufficient.

How should refresh tokens be protected?

Give them limited scope and lifetime, store them securely, bind them to the client, revoke them on risk events, and detect replay. Public clients must use sender-constrained refresh tokens or rotation under current OAuth best practice. Atomic rotation and family revocation are important.

Why does CORS run before Spring Security authentication?

A browser preflight request normally contains no session cookie. If authentication sees it first, it can reject the request before CORS headers are produced. Spring integrates a CORS configuration early in the chain.

What is the correct difference between 401 and 403?

A protected API returns 401 when valid authentication is absent and may include a WWW-Authenticate challenge. It returns 403 when the caller is authenticated but lacks required authorization. Browser flows may redirect an unauthenticated user to login instead.

How do you test Spring Security well?

Test the policy matrix, especially anonymous and authenticated-but-forbidden cases. Use Spring Security's test request processors for focused tests, then integration-test real token validation, identity mapping, CSRF, CORS, sessions, logout, headers, proxy behavior, and negative edge cases.

26. Concise cheat sheet

Need Preferred starting point
Servlet policy SecurityFilterChain plus ordered, explicit request matchers
Service authorization @EnableMethodSecurity and @PreAuthorize
Password storage DelegatingPasswordEncoder with tuned adaptive hash
Browser login Hardened session cookie, CSRF, session fixation protection
Bearer-only API OAuth 2.0 resource server, stateless chain, issuer plus audience checks
Delegated login OIDC authorization code flow with PKCE, state, and nonce
Permissions Least-privilege authorities plus tenant and object-level checks
Authentication failure 401 or login entry point
Authorization failure 403 through an AccessDeniedHandler
Security testing spring-security-test, negative matrix, full decoder integration
  • Deny by default and permit narrowly.
  • Authenticate first, authorize every sensitive operation, and verify object ownership.
  • Never log passwords, cookies, authorization codes, client secrets, or bearer tokens.
  • Do not disable CSRF, CORS support, or headers without a mechanism-specific threat analysis.
  • JWT: verify algorithm, signature, issuer, audience, time, and required claims.
  • OAuth: use authorization code with PKCE, exact redirects, TLS, least scopes, and safe token storage.
  • Plan rotation, revocation, incident response, observability, and negative tests before launch.

27. Official references

Last reviewed · July 2026 · part of knowledge-base