In this tutorial, we’re gonna look at Spring Security Architecture built for JWT Authentication that helps us secure our REST APIs with JWT (JSON Web Token) authentication.
Related Post:
– Spring Security JWT Authentication example – RestAPIs SpringBoot + Spring MVC + Spring JPA + MySQL
Spring Security JWT Authentication architecture
This is diagram for Spring Security/JWT classes that are separated into 3 layers:
– HTTP
– Spring Security
– REST API
Look at the diagram above, we can easily associate these components with Spring Security Authentication process: receive HTTP request, filter, authenticate, store Authentication data, generate token, get User details, authorize, handle exception…
At a glance:
– SecurityContextHolder
provides access to the SecurityContext
.
– SecurityContext
holds the Authentication
and possibly request-specific security information.
– Authentication
represents the principal which includes GrantedAuthority
that reflects the application-wide permissions granted to a principal.
– UserDetails
contains necessary information to build an Authentication
object from DAOs or other source of security data.
– UserDetailsService
helps to create a UserDetails
from a String-based username and is usually used by AuthenticationProvider
.
– JwtAuthTokenFilter
(extends OncePerRequestFilter
) pre-processes HTTP request, from Token, create Authentication
and populate it to SecurityContext
.
– JwtProvider
validates, parses token String or generates token String from UserDetails
.
– UsernamePasswordAuthenticationToken
gets username/password from login Request and combines into an instance of Authentication
interface.
– AuthenticationManager
uses DaoAuthenticationProvider
(with help of UserDetailsService
& PasswordEncoder) to validate instance of UsernamePasswordAuthenticationToken
, then returns a fully populated Authentication
instance on successful authentication.
– SecurityContext
is established by calling SecurityContextHolder.getContext().setAuthentication(…)
with returned authentication
object above.
– AuthenticationEntryPoint
handles AuthenticationException
.
– Access to Restful API is protected by HTTPSecurity and authorized with Method Security Expressions.
Receive HTTP Request
When a HTTP request comes (from a browser, a web service client, an HttpInvoker or an AJAX application – Spring doesn’t care), it will go through a chain of filters for authentication and authorization purposes.
So, it is also true for a User Authentication request, that filter chain will be applied until relevant Authentication Filter is found.
Filter the Request
In this architecture, we add our JwtAuthTokenFilter
(that extends Spring OncePerRequestFilter
abstract class) to the chain of filters.
class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { ... http.addFilterBefore(authenticationJwtTokenFilter(), UsernamePasswordAuthenticationFilter.class); } }
JwtAuthTokenFilter
validates the Token using JwtProvider
:
class JwtAuthTokenFilter extends OncePerRequestFilter { @Autowired private JwtProvider tokenProvider; @Override protected void doFilterInternal(...) { String jwt = getJwt(request); if (jwt!=null && tokenProvider.validateJwtToken(jwt)) { ... } filterChain.doFilter(request, response); } }
Now we have 2 cases:
– Login/SignUp: RestAPI with non-protected APIs -> authenticate Login Request with AuthenticationManager
, if error occurs, handle AuthenticationException
with AuthenticationEntryPoint
.
– With protected Resources:
+ jwt
token is null/invalid -> if Authenticated Error occurs, handle AuthenticationException
with AuthenticationEntryPoint
.
+ jwt
token is valid -> from token, get User information, then create AuthenticationToken
.
Create AuthenticationToken from Token
JwtAuthTokenFilter
extracts username/password from the received token using JwtProvider
, then based on the extracted data, JwtAuthTokenFilter
:
– creates a AuthenticationToken
(that implements Authentication
)
– uses the AuthenticationToken
as Authentication
object and stores it in the SecurityContext
for future filter uses (e.g: Authorization filters).
In this tutorial, we use UsernamePasswordAuthenticationToken
:
// extract user information String username = tokenProvider.getUserNameFromJwtToken(jwt); UserDetails userDetails = userDetailsService.loadUserByUsername(username); // create AuthenticationToken UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()); authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
Store Authentication object in SecurityContext
SecurityContextHolder.getContext().setAuthentication(authentication);
SecurityContextHolder
is the most fundamental object where we store details of the present security context of the application (includes details of the principal). Spring Security uses an Authentication
object to represent this information and we can query this Authentication
object from anywhere in our application:
Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); // currently authenticated user Object principal = authentication.getPrincipal();
getContext()
returns an instance of SecurityContext
interface that holds the Authentication
and possibly request-specific security information.
Delegate AuthenticationToken for AuthenticationManagager
After AuthenticationToken
object was created, it will be used as input parameter for authenticate()
method of the AuthenticationManager
:
public interface AuthenticationManager { Authentication authenticate(Authentication authentication) throws AuthenticationException; }
We can see that AuthenticationManager
is just an interface, the default implementation in Spring Security is ProviderManager
:
public class ProviderManager implements AuthenticationManager, ... { private Listproviders; }
Authenticate with AuthenticationProvider
AuthenticationProviders
ProviderManager
delegates to a list of configured AuthenticationProvider
s, each of them will try to authenticate the User, then either throw an exception or return a fully populated Authentication
object:
public class ProviderManager implements AuthenticationManager, ... { private Listproviders; public Authentication authenticate(Authentication authentication) throws AuthenticationException { for (AuthenticationProvider provider : getProviders()) { ... try { ... result = provider.authenticate(authentication); if (result != null) { copyDetails(authentication, result); break; } } catch (Exception...) {} ... return result; } } }
These are some authentication providers that Spring Framework provides:
- DaoAuthenticationProvider
- PreAuthenticatedAuthenticationProvider
- LdapAuthenticationProvider
- ActiveDirectoryLdapAuthenticationProvider
- JaasAuthenticationProvider
- CasAuthenticationProvider
- RememberMeAuthenticationProvider
- AnonymousAuthenticationProvider
- RunAsImplAuthenticationProvider
- OpenIDAuthenticationProvider
DaoAuthenticationProvider
DaoAuthenticationProvider
works well with form-based logins or HTTP Basic authentication which submits a simple username/password authentication request.
It authenticates the User simply by comparing the password submitted in a UsernamePasswordAuthenticationToken
against the one loaded by the UserDetailsService
(as a DAO):
@Autowired AuthenticationManager authenticationManager; ... Authentication authentication = authenticationManager.authenticate( new UsernamePasswordAuthenticationToken(loginRequest.username, loginRequest.password) );
Configuring this provider is simple with AuthenticationManagerBuilder
:
class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Autowired UserDetailsServiceImpl userDetailsService; @Override public void configure(AuthenticationManagerBuilder authenticationManagerBuilder) throws Exception { authenticationManagerBuilder .userDetailsService(userDetailsService) .passwordEncoder(passwordEncoder()); } @Bean @Override public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); } }
Retrieve User details with UserDetailsService
We can obtain a principal from the Authentication
object. This principal can be cast into a UserDetails
object to lookup the username, password and GrantedAuthority
s.
Therefore, after authenticating is successful, we can simply get UserDetails
from Authentication
object:
UserDetails userDetails = (UserDetails) authentication.getPrincipal(); // userDetails.getUsername() // userDetails.getPassword() // userDetails.getAuthorities()
DaoAuthenticationProvider
also uses UserDetailsService
for getting UserDetails
object. This is the common approach in which we only pass a String-based ‘username’ argument and returns a UserDetails
:
public interface UserDetailsService { UserDetails loadUserByUsername(String username) throws UsernameNotFoundException; }
It is simple to implement UserDetailsService
and easy for us to retrieve authentication information using a persistence strategy:
@Service public class UserDetailsServiceImpl implements UserDetailsService { @Autowired UserRepository userRepository; @Override @Transactional public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { User user = userRepository.findByUsername(username).orElseThrow( () -> new UsernameNotFoundException("User Not Found with -> username or email : " + username)); return UserPrinciple.build(user); // UserPrinciple implements UserDetails } }
Get GrantedAuthority
Another important method provided by Authentication is getAuthorities()
that provides an collection of GrantedAuthority
objects:
public interface Authentication extends Principal, Serializable { Collection extends GrantedAuthority> getAuthorities(); }
A GrantedAuthority
is an authority that is granted to the principal. Such authorities are usually ‘roles’, such as ROLE_ADMIN
, ROLE_PM
, ROLE_USER
…
Protect Resources with HTTPSecurity & Method Security Expressions
Configure HTTPSecurity
To help Spring Security know when we want to require all users to be authenticated, which Exception Handler to be chosen, which filter and when we want it to work. We implement WebSecurityConfigurerAdapter
and provide a configuration in the configure(HttpSecurity http)
method:
@Configuration @EnableWebSecurity public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http.cors().and().csrf().disable(). authorizeRequests() .antMatchers("/api/auth/**").permitAll() .anyRequest().authenticated() .and() .exceptionHandling().authenticationEntryPoint(unauthorizedHandler).and() ...; http.addFilterBefore(authenticationJwtTokenFilter(), UsernamePasswordAuthenticationFilter.class); } }
Method Security Expressions
Spring Security provides some annotations for pre and post-invocation authorization checks, filtering of submitted collection arguments or return values: @PreAuthorize
, @PreFilter
, @PostAuthorize
and @PostFilter
.
To enable Method Security Expressions, we use @EnableGlobalMethodSecurity
annotation:
@EnableGlobalMethodSecurity(prePostEnabled = true) public class WebSecurityConfig extends WebSecurityConfigurerAdapter { ... }
In the code below, we use the most useful annotation @PreAuthorize
to decide whether a method can actually be invoked or not:
@RestController public class TestRestAPIs { @GetMapping("/api/test/user") @PreAuthorize("hasRole('USER') or hasRole('ADMIN')") public String userAccess() { return ">>> User Contents!"; } @GetMapping("/api/test/pm") @PreAuthorize("hasRole('PM') or hasRole('ADMIN')") public String projectManagementAccess() { return ">>> Project Management Board"; } @GetMapping("/api/test/admin") @PreAuthorize("hasRole('ADMIN')") public String adminAccess() { return ">>> Admin Contents"; } }
Handle AuthenticationException – AuthenticationEntryPoint
If the user requests a secure HTTP resource without being authenticated, AuthenticationEntryPoint
will be called. At this time, an AuthenticationException
is thrown, commence()
method on the entry point is triggered:
@Component public class JwtAuthEntryPoint implements AuthenticationEntryPoint { @Override public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException { logger.error("Unauthorized error. Message - {}", e.getMessage()); response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Error -> Unauthorized"); } }