Spring Security

Spring Security

Authentication vs Authorization

Two distinct concerns frequently conflated in design discussions and a common interview opener.

Authentication Authorization
Question answered “Who are you?” “What are you allowed to do?”
When it happens Once, at login/token validation Every protected resource access, after identity is established
Spring Security abstraction AuthenticationManager → Authentication object AccessDecisionManager/AuthorizationManager → GrantedAuthority checks
Failure response 401 Unauthorized 403 Forbidden

Architect-level point: a 403 on a request with no credentials at all is a common API contract bug — if identity was never established, the correct response is 401; 403 should only fire when identity is known but insufficiently privileged. This distinction is worth calling out explicitly in API design reviews since many teams conflate the two status codes.

// Authentication: establishing WHO
Authentication auth = authenticationManager.authenticate(
    new UsernamePasswordAuthenticationToken(username, password));

// Authorization: checking WHAT they can do, post-authentication
@PreAuthorize("hasRole('ADMIN')")
public void deleteOrder(Long id) { ... }

Security Filter Chain

Spring Security is implemented entirely as a chain of servlet Filters registered ahead of DispatcherServlet — tying directly back to the Filters-vs-Interceptors distinction: security is a filter-level concern precisely because it must run before any controller/handler logic, regardless of which controller would eventually be matched.

Request → SecurityContextPersistenceFilter → ... → UsernamePasswordAuthenticationFilter
        → ExceptionTranslationFilter → FilterSecurityInterceptor/AuthorizationFilter → DispatcherServlet

Key filters in the default chain (order matters — this is a frequent whiteboard question):

Filter Responsibility
SecurityContextHolderFilter Loads/clears SecurityContext for the thread per request
UsernamePasswordAuthenticationFilter Handles form-login POST, builds Authentication
BasicAuthenticationFilter Handles HTTP Basic header
BearerTokenAuthenticationFilter / custom JWT filter Validates bearer tokens
ExceptionTranslationFilter Converts AuthenticationException/AccessDeniedException into 401/403 responses
AuthorizationFilter (modern, replaces FilterSecurityInterceptor) Final authorization decision against the request

Modern configuration (Spring Security 6+, lambda DSL — the WebSecurityConfigurerAdapter approach is deprecated):

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .csrf(csrf -> csrf.disable()) // typically disabled for stateless JWT APIs, not for browser session apps
            .sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/api/v1/auth/**", "/actuator/health").permitAll()
                .requestMatchers("/api/v1/admin/**").hasRole("ADMIN")
                .anyRequest().authenticated())
            .addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class)
            .exceptionHandling(ex -> ex
                .authenticationEntryPoint((req, res, e) -> res.sendError(401))
                .accessDeniedHandler((req, res, e) -> res.sendError(403)));
        return http.build();
    }
}

Architect-level nuance — CSRF: disable CSRF only for genuinely stateless, token-authenticated APIs with no browser-cookie-based session. If any part of the app uses session/cookie auth (even alongside JWT for another path), leaving CSRF protection off opens it to cross-site request forgery — a real-world misconfiguration that shows up in security audits more often than it should.


JWT Authentication

Stateless authentication: the server issues a signed token after login; subsequent requests carry it (typically Authorization: Bearer <token>) and are validated without server-side session storage — a natural fit for horizontally scaled microservices where session affinity/replication would otherwise be a burden.

public class JwtAuthFilter extends OncePerRequestFilter {

    @Override
    protected void doFilterInternal(HttpServletRequest req, HttpServletResponse res, FilterChain chain)
            throws ServletException, IOException {
        String header = req.getHeader("Authorization");
        if (header != null && header.startsWith("Bearer ")) {
            String token = header.substring(7);
            try {
                Claims claims = jwtService.parseAndValidate(token);
                UsernamePasswordAuthenticationToken authToken =
                    new UsernamePasswordAuthenticationToken(
                        claims.getSubject(), null, extractAuthorities(claims));
                SecurityContextHolder.getContext().setAuthentication(authToken);
            } catch (JwtException e) {
                SecurityContextHolder.clearContext(); // invalid/expired token — leave unauthenticated, let downstream 401
            }
        }
        chain.doFilter(req, res);
    }
}

Token structure: header.payload.signature — header declares algorithm, payload carries claims (sub, exp, iat, custom claims like roles), signature is HMAC (HS256, shared secret) or RSA/ECDSA (RS256/ES256, public/private key pair).

Architect decision — HS256 vs RS256:

HS256 RS256
Key Single shared secret Private key signs, public key verifies
Use case Single service issuing and validating its own tokens Multiple services need to verify tokens without being able to issue them (microservices architecture)
Risk if leaked Secret leak = forgeable tokens anywhere it’s used Public key leak is harmless by design; only private key compromise is catastrophic

In a microservices estate (relevant to your Smith & Nephew-scale integration context), RS256 is the standard choice — an identity provider holds the private key, downstream services only need the public key to verify, with zero ability to mint tokens themselves.

Refresh token pattern: short-lived access token (minutes) + longer-lived refresh token (days), refresh token stored more restrictively (httpOnly cookie or secure storage) and exchanged via a dedicated /refresh endpoint — limits the blast radius of a leaked access token without forcing frequent re-logins.

Common pitfalls to flag in review: no expiry validation, storing JWTs in localStorage (XSS-exposed) instead of httpOnly cookies for browser clients, and missing signature algorithm allow-listing (the classic “alg: none” JWT forgery vulnerability if the library isn’t configured to reject it).


Method-Level Security

Authorization enforced at the method boundary rather than (or in addition to) the URL boundary — necessary when authorization depends on the data, not just the route.

@Configuration
@EnableMethodSecurity // replaces @EnableGlobalMethodSecurity in Spring Security 6
public class MethodSecurityConfig {}

@Service
public class OrderService {

    @PreAuthorize("hasRole('ADMIN') or @orderSecurity.isOwner(#orderId, authentication.name)")
    public Order getOrder(Long orderId) { ... }

    @PreAuthorize("hasAuthority('ORDER_DELETE')")
    public void deleteOrder(Long orderId) { ... }

    @PostAuthorize("returnObject.customerId == authentication.name")
    public Order findOrder(Long orderId) { ... } // checked AFTER method executes, against the return value
}
Annotation Evaluated Use case
@PreAuthorize Before method execution Standard gate — role/authority/expression check on input params
@PostAuthorize After method execution, against return value When the authorization decision depends on data only available in the result (e.g., “is this the caller’s own record”)
@PreFilter Before, filters a collection argument Strip unauthorized elements from an incoming collection
@PostFilter After, filters a collection return value Strip unauthorized elements from a returned collection

Implementation, again, is the same AOP-proxy mechanism as @Transactional — meaning the self-invocation pitfall applies identically: calling an @PreAuthorize-annotated method via this.method() from within the same class bypasses the check entirely. This is genuinely one of the more dangerous gotchas in Spring, since a missed authorization check fails silently (the call just succeeds) rather than throwing — worth explicitly testing for in security-sensitive code paths.

Architect-level guidance: combine URL-level (authorizeHttpRequests) coarse-grained gating with method-level fine-grained checks — URL rules establish “you must be authenticated to even reach this controller,” method-level rules establish “and specifically, you must own this record or hold this authority.” Relying on URL-level checks alone is insufficient wherever authorization is data-dependent.


Password Encoding

@Bean
public PasswordEncoder passwordEncoder() {
    return new BCryptPasswordEncoder(12); // strength/cost factor — tune based on hardware/latency budget
}
@Service
public class UserRegistrationService {
    private final PasswordEncoder passwordEncoder;

    public void register(String rawPassword) {
        String hash = passwordEncoder.encode(rawPassword); // never store rawPassword
        userRepository.save(new User(hash));
    }

    public boolean verify(String rawPassword, String storedHash) {
        return passwordEncoder.matches(rawPassword, storedHash); // never decode/compare directly
    }
}

Why BCrypt (or Argon2/SCrypt) over plain hashing (SHA-256, MD5): these are adaptive, intentionally slow hashing algorithms with built-in salting — designed specifically to resist brute-force and rainbow-table attacks via a tunable work factor. A fast general-purpose hash (SHA-256) is the wrong tool for passwords precisely because its speed, which is desirable elsewhere, makes brute-forcing cheap.

Algorithm Notes
BCrypt Industry default, Spring Security’s BCryptPasswordEncoder, well-understood, tunable cost factor
Argon2 Winner of the Password Hashing Competition, memory-hard (resists GPU/ASIC cracking better than BCrypt) — increasingly preferred for new systems
SCrypt Also memory-hard, less common in Spring ecosystems than Argon2
DelegatingPasswordEncoder Spring’s default — prefixes stored hashes with {bcrypt}/{argon2} etc., allowing graceful algorithm migration over time without breaking existing stored hashes

Architect-level point: PasswordEncoder.matches() always — never decode and compare strings manually, and never log raw passwords even at DEBUG level (a surprisingly common audit finding). Use Slf4j/logback masking patterns for any field plausibly containing credentials.


OAuth2 Basics

OAuth2 is an authorization delegation framework — it lets a client access a resource on a user’s behalf without ever handling the user’s actual credentials. OpenID Connect (OIDC) layers authentication (identity, via the id_token) on top of OAuth2, which by itself only proves authorization, not identity — a frequently confused distinction.

Core roles:

Role Description
Resource Owner The end user
Client The application requesting access (your service)
Authorization Server Issues tokens after authenticating the user (Keycloak, Okta, Auth0, Cognito, Azure AD)
Resource Server Hosts the protected API, validates incoming tokens

Grant types relevant to architecture decisions:

Grant Use case
Authorization Code (+ PKCE) Standard for web/mobile apps with a user present — the only grant recommended for browser-based flows today
Client Credentials Service-to-service, no user context (machine-to-machine API access)
Refresh Token Exchanging an expired access token for a new one without re-authenticating the user
~~Implicit~~ Deprecated — tokens exposed in URL fragments; superseded by Authorization Code + PKCE even for SPAs
~~Resource Owner Password Credentials~~ Deprecated — requires the client to handle raw credentials, defeats OAuth2’s core purpose

Spring Boot as a Resource Server (validating tokens issued elsewhere — the common microservices pattern):

spring:
  security:
    oauth2:
      resourceserver:
        jwt:
          issuer-uri: https://auth.acme.com/realms/orders
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    http.authorizeHttpRequests(auth -> auth.anyRequest().authenticated())
        .oauth2ResourceServer(oauth2 -> oauth2.jwt(Customizer.withDefaults()));
    return http.build();
}

Spring Security auto-fetches the issuer’s JWKS (public keys) and validates incoming bearer tokens’ signature, expiry, and issuer — no manual JWT parsing filter required, unlike the custom JWT example above (this is the path to prefer whenever an external, standards-compliant identity provider is in play, rather than hand-rolling token issuance/validation).

Architect decision point — build vs delegate: hand-rolled JWT issuance (as shown in the JWT section) is appropriate for a single self-contained service issuing its own tokens. The moment there’s more than one service, or a need for SSO, social login, MFA, or compliance certification, the architecturally sound move is delegating to a dedicated identity provider (Keycloak/Okta/Cognito/Azure AD) speaking standard OAuth2/OIDC, with each service acting purely as a resource server validating tokens — not reinventing identity management per service.