You have indicated that "the token is needed by the service" and you are concerned with passing the token all the way from the web layer into the service layer. In comments on Berin's answer, though, you explain that the purpose of this is role based authorization. There is a simpler implementation possible, in which you create a filter that reads the JWT upon every request and updates the "authentication" object that contains the user's roles. Then you simply open or close different pathways in your application based on the roles.
In a similar Spring Boot application I have used a configuration like this:
@Configuration
public class MySecurityConfiguration extends WebSecurityConfigurerAdapter {
private final MyUserDetailService myUserDetailService;
public MySecurityConfiguration(MyUserDetailService myUserDetailService) {
this.myUserDetailService= myUserDetailService;
}
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().mvcMatchers( // Spring will ignore security controls for the specified endpoints, making them publicly-accessible.
"/favicon.ico", // Spring boot looks for a favicon in src/main/resources and points this URL at it. -- Only when running standalone. Not when running in Tomcat.
"/webjars/**", // This directory is where Maven downloads bootstrap, jquery, etc., at build time
"/public/**" // A directory for static images, CSS, and JS that can be accessed without authentication. Do not use for data that should be private.
);
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.addFilter(new JWTAuthenticationFilter(authenticationManager(),myUserDetailService)) // This filter intercepts the login, authenticates the user, and creates a JWT token in a cookie to authorize subsequent requests..
.addFilterBefore(new JWTAuthorizationFilter(), UsernamePasswordAuthenticationFilter.class) // This filter reads the JWT token from the cookie on subsequent requests, and authorizes the user (or not).
.addFilterBefore(new SecurityContextDeletingFilter(), SecurityContextPersistenceFilter.class) // Prevent the HttpSession from keeping the user logged in after we've deleted the cookie.
.formLogin()
.loginPage("/login") // Our custom Login form. Users will be redirected to it if they are not authenticated.
.defaultSuccessUrl("/")
.and()
.logout()
.logoutUrl("/logout") // This should be the default, but it doesn't hurt to make it explicit.
.logoutSuccessHandler(new LogoutSuccessHandler()) // deletes the JWT authentication cookie and redirects user to the login page
.and()
.authorizeRequests()
.mvcMatchers("/login").permitAll() // The login form must be accessible to anyone, whether or not they're authenticated.
.mvcMatchers("/").authenticated() // The main menu is accessible to any authenticated user.
.mvcMatchers("/admin/**").hasAuthority("ADMIN") // The security pages require the ADMIN authority.
.anyRequest().denyAll(); // deny any other request by default
}
}
My authentication and authorization filters are pretty similar to some of the tutorials out there. I'll skip the authentication filter, since you apparently already have your own. The authorization filter looks like this:
public class JWTAuthorizationFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
Cookie cookie = WebUtils.getCookie(httpServletRequest, "JWT-TOKEN");
if (cookie == null) {
filterChain.doFilter(httpServletRequest, httpServletResponse); // Proceed normally, ignoring the rest of this filter
return;
}
try {
// Our custom JWT-based authorization logic is in getAuthentication(). See below.
UsernamePasswordAuthenticationToken authentication = getAuthentication(cookie.getValue());
SecurityContextHolder.getContext().setAuthentication(authentication);
} catch (ExpiredJwtException e) {
// User presented an expired JWT
} catch (JwtException e) {
// If you get here, the user has a cookie-based JWT but it's invalid for some other reason than expiration.
}
filterChain.doFilter(httpServletRequest,httpServletResponse);
}
private UsernamePasswordAuthenticationToken getAuthentication(String token) {
if( token != null ) {
// parse the token
Claims claims = Jwts.parser()
.setSigningKey(System.getenv("SECRET_KEY"))
.parseClaimsJws(token)
.getBody();
String user = claims.getSubject();
// authorities are transmitted as a comma-delimited string like "USER,ADMIN,SUPERUSER"
String authorityString = (String)claims.get("authorities");
Collection<? extends GrantedAuthority> authorities;
if(!authorityString.isEmpty()) {
authorities = Arrays.asList(authorityString.split(",")).stream()
.map(SimpleGrantedAuthority::new).collect(Collectors.toList());
} else {
authorities = null;
}
if( user != null ) {
return new UsernamePasswordAuthenticationToken(user, null, authorities);
}
return null;
}
return null;
}
}
One issue I found is that the authentication becomes attached to the session (the JSESSION_ID cookie), which made it impossible for my users to sign out -- I would delete the JWT cookie but the other cookie would keep them logged in. So I added a custom SecurityContextDeletingFilter
which clears the security context on every request. Here is its code:
public class SecurityContextDeletingFilter extends GenericFilterBean {
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
final HttpServletRequest request = (HttpServletRequest) servletRequest;
final HttpSession session = request.getSession();
if( session.getAttribute(HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY) != null ) {
session.removeAttribute(HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY);
}
filterChain.doFilter(servletRequest,servletResponse);
}
}
To sum up, by requiring the "ADMIN" authority for URLs under /admin/
, I can give access to any service-tier logic at those endpoints and know that they are secured by the roles encoded in the JWT token. I do not need to pass the token to the service tier to accomplish this.