Securing a REST API step by step (TLS, authentication, authorisation, validation, rate limiting, secrets, headers, logging), Spring Security options (Basic, JWT resource server, OAuth2/OIDC, mTLS), JWT structure and pitfalls, a banking-grade security design, authentication and authorisation in Spring Boot, service-to-service security, URL vs method security, the modern core classes (SecurityFilterChain, not WebSecurityConfigurerAdapter), and the complete stateless JWT flow.
Published September 25, 2026
"How would you secure a REST API?" is a breadth question. Answer it in layers:
Then go deep on the Spring Security specifics, using Spring Security 6 APIs: SecurityFilterChain beans and authorizeHttpRequests. WebSecurityConfigurerAdapter was removed, and naming it as a core class is a red flag.
Short answer:
@PreAuthorize).Learn it in depth → Spring Security Overview
Short answer: Spring Security (spring-boot-starter-security) supports:
spring-boot-starter-oauth2-resource-server): the standard for APIs. It validates the signature, issuer, audience and expiry against the identity provider's JWKS. Opaque tokens are validated by introspection.-oauth2-client): for applications that log users in through Keycloak, Okta, Auth0, Entra ID or Google.@EnableMethodSecurity, @PreAuthorize).DelegatingPasswordEncoder).Short answer: A JWT is three Base64URL parts separated by dots: header.payload.signature.
alg (for example RS256 or ES256), typ, and kid, the key ID used for rotation.iss (issuer), sub (subject, the user ID), aud (audience), exp (expiry), iat (issued at), nbf, jti (a unique ID).scope/scp), tenant.Key points to cover:
alg: none and algorithm-confusion tricks);exp/nbf with a small clock skew;iss and aud.jti) if you need immediate revocation.Learn it in depth → JWT Authentication
Short answer: Add spring-boot-starter-security. Then define:
UserDetailsService backed by a database, LDAP, or an OAuth2/OIDC provider);Do both as beans:
@Configuration
@EnableMethodSecurity
class SecurityConfig {
@Bean
SecurityFilterChain api(HttpSecurity http) throws Exception {
return http
.csrf(csrf -> csrf.disable()) // stateless bearer-token API (no cookies)
.sessionManagement(s -> s.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(auth -> auth
.requestMatchers("/actuator/health/**", "/public/**").permitAll()
.requestMatchers(HttpMethod.GET, "/api/products/**").hasAuthority("SCOPE_catalog.read")
.requestMatchers("/api/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()) // deny-by-default posture
.oauth2ResourceServer(o -> o.jwt(jwt -> jwt.jwtAuthenticationConverter(rolesFromClaim())))
.build();
}
@Bean PasswordEncoder passwordEncoder() { return PasswordEncoderFactories.createDelegatingPasswordEncoder(); } // for local users
}
spring:
security:
oauth2:
resourceserver:
jwt:
issuer-uri: https://auth.shopin.example/realms/shop # discovers the JWKS; validates iss and exp
audiences: orders-api
Key points to cover:
Authentication stored in the SecurityContext.AuthorizationManagers check URL rules, and @PreAuthorize checks methods.AuthenticationEntryPoint). An authenticated request without permission gets a 403 (from the AccessDeniedHandler).Learn it in depth → Authentication Mechanics
Short answer: The same foundation as Q4, stated as the steps an interviewer expects:
UserDetailsService (a JPA-backed user table), plus a PasswordEncoder (BCrypt or Argon2). Or delegate to an identity provider with OAuth2/OIDC.SecurityFilterChain, with URL rules by role or authority.@WithMockUser, jwt() post-processors and spring-security-test.@PreAuthorize("hasRole('CUSTOMER') and @orderAccess.isOwner(#orderId, authentication)")
public OrderDto getOrder(UUID orderId) { ... }
Learn it in depth → Role-Based Access Control
Short answer: Everything from Q1, raised to regulated-industry level:
Short answer:
AuthorizationPolicy, and Kubernetes NetworkPolicies).@Bean
RestClient inventoryClient(RestClient.Builder builder, OAuth2AuthorizedClientManager clients) {
var interceptor = new OAuth2ClientHttpRequestInterceptor(clients); // Spring Security 6.4+
interceptor.setClientRegistrationIdResolver(req -> "inventory-client"); // client-credentials registration
return builder.baseUrl("https://inventory.internal").requestInterceptor(interceptor).build();
}
Short answer:
SecurityFilterChain (authorizeHttpRequests). It's enforced by the AuthorizationFilter, before the request reaches the controller, based on the path, the HTTP method and the authorities. It's coarse-grained, central and cheap. For example, "/api/admin/** needs ADMIN".@EnableMethodSecurity plus @PreAuthorize/@PostAuthorize/@PreFilter/@PostFilter (or @Secured, @RolesAllowed). It's enforced by an AOP proxy around the bean method. It's fine-grained, can use method arguments and return values in SpEL (ownership checks), and protects the service regardless of the entry point: REST, messaging or scheduled jobs.Key points to cover:
Short answer: The core building blocks, in Spring Security 6:
SecurityFilterChain (a bean) and HttpSecurity: configure the ordered filter chain. The DelegatingFilterProxy/FilterChainProxy connect it to the servlet container.AuthenticationManager (usually ProviderManager) and AuthenticationProviders, such as DaoAuthenticationProvider and JwtAuthenticationProvider.UserDetailsService/UserDetails, and PasswordEncoder.Authentication, SecurityContext and SecurityContextHolder (thread-local by default), plus SecurityContextRepository.AuthorizationManager (URL and method authorisation), and GrantedAuthority.AuthenticationEntryPoint (401) and AccessDeniedHandler (403).Spring MVC vs Spring Boot:
spring-security-web, -config), register the filter (AbstractSecurityWebApplicationInitializer), and write all of the configuration yourself.spring-boot-starter-security brings everything, and auto-configures a default filter chain and user. Your SecurityFilterChain bean replaces the defaults. The core classes are the same.Common trap: listing WebSecurityConfigurerAdapter as a core class. It was deprecated in 5.7, and removed in Spring Security 6 (Boot 3). Configure with SecurityFilterChain beans and the lambda DSL.
Short answer: The preferred design delegates token issuing to an authorization server (Keycloak, Okta, or Spring Authorization Server), and makes your API a resource server. If the API must issue tokens itself, the flow is:
/auth/login over HTTPS. The AuthenticationManager authenticates them (DaoAuthenticationProvider + UserDetailsService + PasswordEncoder).kid. It carries sub, the roles or scopes, iss, aud, exp and jti. Also issue a refresh token: opaque, stored hashed on the server, rotated on every use, and sent as an HttpOnly Secure SameSite cookie for browser clients.Authorization: Bearer <access-token>.BearerTokenAuthenticationFilter, from the resource-server support, extracts the token. A JwtDecoder checks the signature, algorithm, exp/nbf, iss and aud. A converter maps the claims to authorities, and the Authentication goes into the SecurityContext for this request only. No session is created.@PreAuthorize decide. The response is 401 for an invalid or expired token, and 403 for missing permission./auth/refresh with the refresh token. The server verifies and rotates it; if an old one is reused, it detects the theft and revokes the whole token family.jti denylist if you need immediate cut-off.@Bean
JwtDecoder jwtDecoder(RSAPublicKey publicKey) {
NimbusJwtDecoder decoder = NimbusJwtDecoder.withPublicKey(publicKey).signatureAlgorithm(SignatureAlgorithm.RS256).build();
decoder.setJwtValidator(JwtValidators.createDefaultWithValidators( // default = exp/nbf (+ issuer via createDefaultWithIssuer)
new JwtIssuerValidator("https://api.shopin.example"),
new JwtClaimValidator<List<String>>("aud", aud -> aud != null && aud.contains("orders-api"))));
return decoder;
}
Common traps:
localStorage: any XSS steals them. Prefer HttpOnly cookies, or a backend-for-frontend.Learn it in depth → OAuth2 & Social Login Basics
Q: When should CSRF protection be disabled?
A: Only for APIs authenticated purely by headers (bearer tokens) that browsers don't attach automatically. If authentication uses cookies (sessions, or a JWT in a cookie), keep CSRF on (Spring's CookieCsrfTokenRepository for SPAs), or rely on SameSite plus custom-header checks with care.
Q: How do you rotate JWT signing keys without downtime?
A: Publish the keys through a JWKS endpoint, identified by kid. Add the new key to the JWKS first, start signing with it, and keep the old public key published until every token signed with it has expired. Resource servers pick up the change from the cached JWKS automatically.
Q: How do you prevent IDOR (Broken Object Level Authorization)?
A: Never trust IDs from the client alone. Check ownership or tenancy on every object access, in the service layer: @PostAuthorize("returnObject.ownerId == authentication.name"), or repository queries scoped by user or tenant (findByIdAndOwnerId). Test it explicitly.
Q: Where should rate limiting live? A: Mainly at the edge (the API gateway or WAF), per client, IP or token, so abuse is stopped cheaply. Add finer, per-user or per-operation limits in the service (Bucket4j with Redis) for expensive endpoints like login, OTP and search.