⬆️ ⬇️

Authentication using Spring Security and JWT tokens

Hello! Habr is alive! This post is unlikely to collect a bunch of views and comments, but I hope it will help a little Habra.



This article will look at the principle of authentication in web applications on the Spring platform using a relatively new authentication mechanism - JSON Web Token (JWT) . This mechanism has already been tested and implemented for many programming languages.





')

Using a token allows the server to not worry about saving the state between requests (HTTP session), to reduce the number of queries to the database - the data necessary for recovery can be stored in the token. Directly about the JWT token: the server mixes the payload in JSON format (header and body) with the secret key and generates a hash, attaching it as a signature to the payload. The payload is encoded by the base64Url algorithm, so naturally, you should not transfer secret data in the token. JWT standard does not provide payload encryption. Encrypt separately yourself if you wish, and the token's task is only to provide authentication.



It is assumed that the reader is familiar with the basics of Spring Secutity. You can read about it here.



one). Token generation


For my example, I took one of the implementations of the JWT specification. The token is generated as follows:



package com.example.security; import io.jsonwebtoken.JwtBuilder; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm; import io.jsonwebtoken.impl.crypto.MacProvider; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.core.userdetails.User; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.stereotype.Service; import java.util.*; @Service public class GetTokenServiceImpl implements GetTokenService { @Autowired private UserDetailsService userDetailsService; @Override public TokenObject getToken(String username, String password) throws Exception { if (username == null || password == null) return null; User user = (User) userDetailsService.loadUserByUsername(username); Map<String, Object> tokenData = new HashMap<>(); if (password.equals(user.getPassword())) { tokenData.put("clientType", "user"); tokenData.put("userID", user.getUserId().toString()); tokenData.put("username", authorizedUser.getUsername()); tokenData.put("token_create_date", new Date().getTime()); Calendar calendar = Calendar.getInstance(); calendar.add(Calendar.YEAR, 100); tokenData.put("token_expiration_date", calendar.getTime()); JwtBuilder jwtBuilder = Jwts.builder(); jwtBuilder.setExpiration(calendar.getTime()); jwtBuilder.setClaims(tokenData); String key = "abc123"; String token = jwtBuilder.signWith(SignatureAlgorithm.HS512, key).compact(); return token; } else { throw new Exception("Authentication error"); } } } 


As a result, we get a string like <Heading>. <Body>. <Signature> , which we send to the client



Now to Spring Security. To implement our own authentication mechanism, we need to implement our filter and authentication manager .



2). Filter implementation


A filter is an object of a class that implements the javax.servlet.Filter interface, which intercepts requests to specific URLs and performs some actions. If there are several filters, they form a chain of filters — an HTTP request after reception by the application passes through this chain. Each filter in the chain can process the request, skip it to the following filters in the chain, or not skip it, immediately sending a response to the client.



The task of our filter is to transfer the token from the request to the authentication manager and, in case of successful authentication, set the security context of the application.



 package com.example.security; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter; import javax.servlet.*; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; public class TokenAuthenticationFilter extends AbstractAuthenticationProcessingFilter { public TokenAuthenticationFilter() { super("/rest/**"); setAuthenticationSuccessHandler((request, response, authentication) -> { SecurityContextHolder.getContext().setAuthentication(authentication); request.getRequestDispatcher(request.getServletPath() + request.getPathInfo()).forward(request, response); }); setAuthenticationFailureHandler((request, response, authenticationException) -> { response.getOutputStream().print(authenticationException.getMessage()); }); } @Override public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException { String token = request.getHeader("token"); if (token == null) token = request.getParameter("token"); if (token == null) { TokenAuthentication authentication = new TokenAuthentication(null, null); authentication.setAuthenticated(false); return authentication; } TokenAuthentication tokenAuthentication = new TokenAuthentication(token); Authentication authentication = getAuthenticationManager().authenticate(tokenAuthentication); return authentication; } @Override public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { super.doFilter(req, res, chain); } } 




We inherited from the abstract class org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter , which is specifically designed for authentication. If the URL of the request coincides with the pattern "/ rest / **", the function call attemptAccesshentication () will occur.

Also in the constructor, we set up two handlers — AuthenticationSuccessHandler and AuthenticationFailureHandler . If attemptAuthentication returns an Authentication object, then the first handler will trigger, the second handler will fire when the AuthenticationException exception is attempted by the attemptAthentication method.

As we can see, upon successful authentication, we set the security context of the application via SecurityContextHolder.getContext (). SetAuthentication (authentication) . The context thus set is the ThreadLocal variable, i.e. available while the workflow is alive with the client. After setting the context, we send the user request to the servlet with the originally requested URL.



3). Authentication Manager.


The authentication manager is a class object that implements the org.springframework.security.authentication.AuthenticationManager interface with the only authenticate () method. This method needs to pass a partially populated object that implements the org.springframework.security.core.Authentication interface (application security context).

The task of the authentication manager is to complete the Authentication object in the case of successful authentication and return it. When filling you need to set the user ( principal ), his rights ( authorities ), execute setAuthenticated (true) . In case of failure, the authentication manager should throw an AuthenticationException exception.



Here is an example implementation of the org.springframework.security.core.Authentication interface:



 package com.example.security; import org.springframework.security.core.Authentication; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.userdetails.UserDetails; import javax.servlet.http.HttpServletRequest; import java.util.Collection; import java.util.List; import java.util.Map; public class TokenAuthentication implements Authentication { private String token; private Collection<? extends GrantedAuthority> authorities; private boolean isAuthenticated; private UserDetails principal; public TokenAuthentication(String token) { this.token = token; this.details = request; } public TokenAuthentication(String token, Collection<SimpleGrantedAutority> authorities, boolean isAuthenticated, UserDetails principal) { this.token = token; this.authorities = authorities; this.isAuthenticated = isAuthenticated; this.principal = principal; } @Override public Collection<? extends GrantedAuthority> getAuthorities() { return authorities; } @Override public Object getCredentials() { return null; } @Override public Object getDetails() { return details; } @Override public String getName() { if (principal != null) return ((UserDetails) principal).getUsername(); else return null; } @Override public Object getPrincipal() { return principal; } @Override public boolean isAuthenticated() { return isAuthenticated; } @Override public void setAuthenticated(boolean b) throws IllegalArgumentException { isAuthenticated = b; } public String getToken() { return token; } } 




Here is the authentication manager implementation:



 package com.example.security; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.impl.DefaultClaims; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.AuthenticationServiceException; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; import org.springframework.security.authentication.AuthenticationServiceException import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.userdetails.User; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.stereotype.Service; import org.springframework.security.core.GrantedAuthority; import javax.servlet.http.HttpServletRequest; import java.util.*; import java.util.function.Function; import java.util.stream.Collectors; @Service public class TokenAuthenticationManager implements AuthenticationManager { @Autowired private UserDetailsService userDetailsService; @Override public Authentication authenticate(Authentication authentication) throws AuthenticationException { try { if (authentication instanceof TokenAuthentication) { TokenAuthentication readyTokenAuthentication = processAuthentication((TokenAuthentication) authentication); return readyTokenAuthentication; } else { authentication.setAuthenticated(false); return authentication; } } catch (Exception ex) { if(ex instanceof AuthenticationServiceException) throw ex; } } private TokenAuthentication processAuthentication(TokenAuthentication authentication) throws AuthenticationException { String token = authentication.getToken(); String key = "key123"; DefaultClaims claims; try { claims = (DefaultClaims) Jwts.parser().setSigningKey(key).parse(token).getBody(); } catch (Exception ex) { throw new AuthenticationServiceException("Token corrupted"); } if (claims.get("TOKEN_EXPIRATION_DATE", Long.class) == null) throw new AuthenticationServiceException("Invalid token"); Date expiredDate = new Date(claims.get("TOKEN_EXPIRATION_DATE", Long.class)); if (expiredDate.after(new Date())) return buildFullTokenAuthentication(authentication, claims); else throw new AuthenticationServiceException("Token expired date error"); } private TokenAuthentication buildFullTokenAuthentication(TokenAuthentication authentication, DefaultClaims claims) { User user = (User) userDetailsService.loadUserByUsername(claims.get("USERNAME", String.class)); if (user.isEnabled()) { Collection<GrantedAutority> authorities = user.getAuthorities(); TokenAuthentication fullTokenAuthentication = new TokenAuthentication(authentication.getToken(), authorities, true, user); return fullTokenAuthentication; } else { throw new AuthenticationServiceException("User disabled");; } } } 




four). How to put it all together


First, you need to install a filter. This can be done in 2 ways.



The first way is to define a filter in the web.xml file of our application.



  <filter> <filter-name>springSecurityTokenFilter</filter-name> <filter-class>com.example.security.TokenAuthenticationFilter</filter-class> </filter> <filter-mapping> <filter-name>springSecurityTokenFilter</filter-name> <url-pattern>/rest/**</url-pattern> </filter-mapping> 


With this method, the authentication manager must be set in the filter constructor immediately, since the filter instance will not be available in the context of the Spring application. If you need to have a filter or authentication manager as Spring Bins, you must use the second method.



The second way is to install the filter in the Spring Security configuration.



For example, we show the configuration using Java Config



 package com.example.security; import com.example.security.RestTokenAuthenticationFilter; import com.example.security.TokenAuthenticationManager; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; @Configuration @EnableWebSecurity @EnableGlobalMethodSecurity(securedEnabled = true) public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired @Qualifier("userDetailsService") UserDetailsService userDetailsService; @Autowired TokenAuthenticationManager tokenAuthenticationManager; @Override protected void configure(HttpSecurity http) throws Exception { http .headers().frameOptions().sameOrigin() .and() .addFilterAfter(restTokenAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class) .authorizeRequests() .antMatchers("/rest/*").authenticated() } @Bean(name = "restTokenAuthenticationFilter") public RestTokenAuthenticationFilter restTokenAuthenticationFilter() { RestTokenAuthenticationFilter restTokenAuthenticationFilter = new RestTokenAuthenticationFilter(); tokenAuthenticationManager.setUserDetailsService(userDetailsService); restTokenAuthenticationFilter.setAuthenticationManager(tokenAuthenticationManager); return restTokenAuthenticationFilter; } } 


In line

.addFilterAfter (restTokenAuthenticationFilter (), UsernamePasswordAuthenticationFilter.class)

we added our filter to the filter chain after the standard UsernamePasswordAuthenticationFilter filter.



This completes the basic configuration of the authentication mechanism in Spring Security using JSON Web Token.



I wish you all success!



Thanks for attention!

Source: https://habr.com/ru/post/278411/



All Articles