Spring Security Fundamentals

Authentication vs Authorization

18 min Lesson 3 of 13

Authentication vs Authorization

Every secured system rests on two distinct pillars: authentication (who are you?) and authorization (what are you allowed to do?). These questions are logically sequential and must never be conflated. Confusing them is one of the most common sources of security bugs in real-world applications — a misconfigured authorization rule that lets an authenticated user reach a resource they should never see is a data breach, not a login failure.

Spring Security handles both concerns, but through separate, well-defined subsystems. This lesson gives you the conceptual foundation and the practical Spring Security 6 API surface for each.

Authentication — Proving Identity

Authentication is the process of verifying that a principal (a user, a service, or a device) is who it claims to be. The classic mechanism is a username and password, but the concept is broader: a JSON Web Token, an X.509 client certificate, an OAuth 2 access token, or a SAML assertion are all forms of credentials that can be used to authenticate a principal.

In Spring Security the result of a successful authentication is an Authentication object stored in the SecurityContext. It contains three things:

  • Principal — who was authenticated (typically a UserDetails object).
  • Credentials — what was used to prove identity (erased after authentication for security).
  • Authorities — the set of granted permissions (GrantedAuthority instances) associated with this principal.

You can inspect the currently authenticated principal anywhere in your application:

import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.userdetails.UserDetails; public class SecurityUtils { public static String currentUsername() { Authentication auth = SecurityContextHolder.getContext().getAuthentication(); if (auth == null || !auth.isAuthenticated()) { return "anonymous"; } Object principal = auth.getPrincipal(); if (principal instanceof UserDetails ud) { return ud.getUsername(); // pattern-matched with Java 16 instanceof } return principal.toString(); // fallback for OAuth2 / JWT subjects } }
The SecurityContext is thread-local by default. Spring Security stores the Authentication in a ThreadLocal, so it is automatically available throughout the life of a servlet request without being passed as a parameter. In reactive (WebFlux) applications the context is stored in the reactive pipeline instead — never assume thread-local semantics there.

Authorization — Enforcing What Is Allowed

Authorization runs after authentication. Once the system knows who the caller is, it must decide whether that caller may perform the requested action on the requested resource. Spring Security expresses authorization in terms of granted authorities (roles and permissions) that are checked against rules you configure.

There are two major places where Spring Security enforces authorization:

  1. HTTP layer — rules applied to incoming requests before they reach a controller, configured via SecurityFilterChain.
  2. Method layer — annotations like @PreAuthorize applied to individual service or controller methods, enforced via AOP.

A minimal authorization configuration in Spring Security 6 looks like this:

import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.web.SecurityFilterChain; @Configuration @EnableWebSecurity public class SecurityConfig { @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http .authorizeHttpRequests(auth -> auth .requestMatchers("/public/**").permitAll() // no authentication required .requestMatchers("/admin/**").hasRole("ADMIN") // must be authenticated + have ROLE_ADMIN .requestMatchers("/api/**").hasAuthority("API_READ") // specific permission .anyRequest().authenticated() // everything else: authenticated only ) .formLogin(form -> form .loginPage("/login") .permitAll() ); return http.build(); } }
Roles vs Authorities: A role is a named authority with the prefix ROLE_. When you call hasRole("ADMIN"), Spring Security checks for an authority named ROLE_ADMIN — it prepends the prefix automatically. When you call hasAuthority("API_READ"), it checks for that exact string. Use roles for broad job functions (ADMIN, USER, MANAGER) and fine-grained authorities for specific capabilities (INVOICE_WRITE, REPORT_READ).

Why the Order Matters — and Why They Must Stay Separate

Authentication always runs first. If a request cannot be authenticated, there is nothing to authorize — the request is rejected with 401 Unauthorized. Only after a valid Authentication object is in the SecurityContext do the authorization rules fire. A rejected authorization returns 403 Forbidden.

These two HTTP status codes carry specific semantic meaning that your API clients depend on:

  • 401 — "You have not proven who you are. Present valid credentials."
  • 403 — "I know who you are, but you are not allowed to do this."
Returning 403 when you should return 401 leaks information. If your API returns 403 Forbidden for an unauthenticated request to a protected endpoint, you are telling attackers that the endpoint exists and is protected. Always return 401 first, reserve 403 for authenticated-but-not-authorized cases. Spring Security does this correctly by default — only override AuthenticationEntryPoint and AccessDeniedHandler if you understand the implications.

Authentication and Authorization in a Stateless REST API

Traditional web applications store the Authentication in an HTTP session (stateful). Stateless REST APIs — and microservices — take a different approach: the client presents a self-contained token (typically a JWT) on every request, and the server verifies it without consulting any session store.

In this model the two pillars shift slightly:

  • Authentication happens at the token-verification step: the JWT's signature is verified, its expiry is checked, and the embedded subject and claims are extracted to rebuild an Authentication object in the SecurityContext.
  • Authorization then proceeds identically: the authorities embedded in the token (or looked up from a database) are checked against the rules.
// Typical claims in a JWT used for both AuthN and AuthZ { "sub": "alice@example.com", // authentication: who is this? "roles": ["ROLE_USER"], // authorization: what can she do? "permissions": ["INVOICE_READ"], // fine-grained authorization "exp": 1718000000 // expiry: part of authentication validity }
Distributed systems trade-off: Embedding authorities in a JWT means the token is self-contained and your service does not need a database call to authorize each request — excellent for latency. The downside is that revoked permissions do not take effect until the token expires. Short expiry times (5–15 minutes) plus refresh tokens are the standard mitigation. Lesson 10 of this tutorial (JWT) explores this in depth.

Spring Security's Internal Contract

Internally, Spring Security separates the two concerns at the API level:

  • AuthenticationManager / AuthenticationProvider — responsible for authentication. They take an unauthenticated token (e.g. UsernamePasswordAuthenticationToken with raw credentials), validate it, and return a fully populated, trusted Authentication object.
  • AccessDecisionManager / AuthorizationManager — responsible for authorization. They receive the authenticated Authentication and a secured object (an HTTP request or a method invocation) and decide whether to grant access.

You will configure and extend these interfaces throughout this tutorial. For now the key takeaway is architectural: Spring Security's design enforces the separation of authentication and authorization at the type level, making it harder to accidentally conflate them.

Summary

Authentication answers "who are you?" and produces a trusted Authentication object in the SecurityContext. Authorization answers "what may you do?" and checks that object's authorities against the rules you define. Authentication always happens first; its failure yields 401, while an authorization failure yields 403. Spring Security enforces the boundary between these two concerns at the API level, both in the HTTP filter chain and in the method-security AOP layer. Keeping this mental model clear will save you from the most common class of security misconfiguration bugs.