It has been a long time since i posted any java related blog post. Today it is a time i came up from my busy schedule. So in this post i am just only going to demonstrate how to add JWT authentication for small boot application which will use redis for saving its user’s session and information. There are many way we can use session but lets try this time with Redis as primary source for session storage.
I have my redis server in my VMWare which is opened in default port 6379. So now all i need is small spring boot application to get started. Lets start a new project with clean maven project and some dependencies essential for making JWT Token based session using redis.
1. Project Create
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>1.5.7.RELEASE</version> </parent> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.7.0</version> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </dependency> <dependency> <groupId>redis.clients</groupId> <artifactId>jedis</artifactId> </dependency> </dependencies> |
Here we have just added <parent>
for making the project to inherit all the stuffs from the spring-boot-starter-parent
. So now we have added spring-boot-starter-web, spring-boot-starter-data-redis, spring-security, jwt, jedis(redis) and lombok in our project which are essential dependency for starting our spring boot rest application with redis support.
The first thing we do is Create main Application file which will help the Spring Boot application to start.
1 2 3 4 5 6 7 8 9 10 11 12 |
@SpringBootApplication @EnableAutoConfiguration public class Application { //... Stuffs to be added ... public static void main(String[] args) { SpringApplication.run(Application.class, args); } } |
So this is right now bare server with built-in spring security. Now lets create a small User resource which authenticated user can only view them.
2. Redis Configuration
This configuration is actually dependent on application.properties which contains the key-value map of redis params.
application.properties
1 2 3 |
redis.host = localhost redis.port = 6379 redis.pass = |
These values will be extracted from spring to configure redis connection and make ConnectionFactory available within an application.
RedisConfig.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
@Configuration public class RedisConfig { @Value("${redis.host}") private String redisHost; @Value("${redis.port}") private Integer redisPort; @Value("${redis.pass}") private String redisPass; @Bean @Primary JedisConnectionFactory jedisConnectionFactory() throws Exception { JedisConnectionFactory factory = new JedisConnectionFactory(); factory.setHostName(redisHost); factory.setPort(redisPort); if (redisPass != null) { factory.setPassword(redisPass); } factory.setUsePool(true); return factory; } @Bean RedisTemplate< String, Object> redisTemplate() throws Exception { final RedisTemplate< String, Object> template = new RedisTemplate< String, Object>(); template.setConnectionFactory(jedisConnectionFactory()); template.setKeySerializer(new StringRedisSerializer()); template.setHashValueSerializer(new Jackson2JsonRedisSerializer<>(Object.class)); template.setValueSerializer(new Jackson2JsonRedisSerializer<>(Object.class)); return template; } } |
So now all the redis part of configuration is done . There are templates and connection factory in this RedisConfig.java. Templates are used for saving and extracting the complex data in redis and connection factory is for connection pooling of redis.
3. JWT Implementation
For making the JWT to work we need to add two filters on every request incoming to server. The first one would be for authenticating user with their credentials (user, password) and another would be for validating their JWT token and authorizing the requested resource. But before moving to those filter lets start creating a service which helps us to generate JWT token for us and validate that token against the redis.
TokenAuthenticationService.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 |
public class TokenAuthenticationService { private RedisService service; private long EXPIRATIONTIME = 1000 * 60 * 60; // 1 hr private String secret; private String tokenPrefix = "Bearer"; private String headerString = "Authorization"; public TokenAuthenticationService(RedisService service) { this.service = service; secret = Sha512DigestUtils.shaHex(System.getenv("ENC_KEY")); } public void addAuthentication(HttpServletResponse response, AuthenticationTokenImpl auth) { // We generate a token now. Map<String, Object> claims = new HashMap<>(); claims.put("username", auth.getPrincipal()); claims.put("hash", auth.getHash()); String JWT = Jwts.builder() .setSubject(auth.getPrincipal().toString()) .setClaims(claims) .setExpiration(new Date(System.currentTimeMillis() + EXPIRATIONTIME)) .signWith(SignatureAlgorithm.HS512, secret) .compact(); response.addHeader(headerString, tokenPrefix + " " + JWT); } public Authentication getAuthentication(HttpServletRequest request) { String token = request.getHeader(headerString); if (token == null) { return null; } //remove "Bearer" text token = token.replace(tokenPrefix, "").trim(); //Validating the token if (token != null && !token.isEmpty()) { // parsing the token.` Claims claims = null; try { claims = Jwts.parser() .setSigningKey(secret) .parseClaimsJws(token).getBody(); } catch ( Exception e) { return null; } //Valid token and now checking to see if the token is actally expired or alive by quering in redis. if (claims != null && claims.containsKey("username")) { String username = claims.get("username").toString(); String hash = claims.get("hash").toString(); SessionUser user = (SessionUser) service.getValue(String.format("%s:%s", username,hash), SessionUser.class); if (user != null) { AccessTokenBasedAuth auth = new AccessTokenBasedAuth(user.getUsername(), Collections.emptyList()); auth.setDetails(user); auth.authenticate(); return auth; } else { return new UsernamePasswordAuthenticationToken(null, null); } } } return null; } } |
If we look into addAuthentication()
there we can see new JWT token is generated using the SHA encrypted password , the password is actually pulled from the system environment variable. Now for the authenticating part we are just using standard header format “Authorization: Bearer aawd383ec9930k… ” . It just checks if there exist any record matching with the key (user:hash) . Right now user can login with many session but in future we can do those restriction or view all the session and delete any which user wants to. So typically this service class will just create JWT Token and also validates the token which was sent on request payload header.
There is still a need of authentication part which really validates the user’s credential so lets create the part of authentication .
AuthenticationProviderImpl.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
public class AuthenticationProviderImpl implements org.springframework.security.authentication.AuthenticationProvider { private RedisService service; public AuthenticationProviderImpl(RedisService service) { this.service = service; } @Override public Authentication authenticate(Authentication authentication) throws AuthenticationException { String username = authentication.getPrincipal() + ""; String password = authentication.getCredentials() + ""; if (username == null || username.length() < 5) { throw new BadCredentialsException("Username not found."); } if (password.length() < 5) { throw new BadCredentialsException("Wrong password."); } //Right now just authenticate on the basis of the user=pass if (username.equalsIgnoreCase(password)) { SessionUser u = new SessionUser(); u.setUsername(username); u.setCreated(new Date()); AuthenticationTokenImpl auth = new AuthenticationTokenImpl(u.getUsername(), Collections.emptyList()); auth.setAuthenticated(true); auth.setDetails(u); service.setValue(String.format("%s:%s", u.getUsername().toLowerCase(), auth.getHash()), u, TimeUnit.SECONDS, 3600L, true); return auth; } else { } return null; } @Override public boolean supports(Class<?> type) { return type.equals(UsernamePasswordAuthenticationToken.class); } } |
This class will authenticate the request and create new session for user then saves that user’s data in redis for making it available for certain time interval i.e. session time. The SessionUser is the object that we save inside of redis with it’s key formatted as “username:hash” . So if you are wondering what hash() really is then its time to show you small snippet of AuthenticationTokenImpl.java. This class is just a subclass of AbstractAuthenticationToken which adds some extra parameter to be able to handle on.
AuthenticationTokenImpl.java
1 2 3 4 5 6 7 8 9 |
public class AuthenticationTokenImpl extends AbstractAuthenticationToken{ //...other codes public String getHash() { return DigestUtils.md5DigestAsHex(String.format("%s_%d", username, ((SessionUser) getDetails()).getCreated().getTime()).getBytes()); } } |
4. Spring Security and Request Filters
The master piece of all is the spring security which maintains all our resources to be secured and only allow after there is a valid token while doing a request. When it comes for restricting access resource in spring it comes quiet handy to manage them i.e. WebSecurityConfigurerAdapter we need to extend this class to create our own customized security.
SecurityConfiguration.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
public class SecurityConfiguration extends WebSecurityConfigurerAdapter { @Autowired private TokenAuthenticationService tokenService; @Autowired private RedisService redisService; @Override protected AuthenticationManager authenticationManager() throws Exception { return new ProviderManager(Arrays.asList((AuthenticationProviderImpl) new AuthenticationProviderImpl(redisService))); } @Bean public FilterRegistrationBean corsFilter() { UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); CorsConfiguration config = new CorsConfiguration(); config.setAllowCredentials(true); config.addAllowedOrigin("*"); config.addAllowedHeader("*"); config.addAllowedMethod("*"); source.registerCorsConfiguration("/**", config); FilterRegistrationBean bean = new FilterRegistrationBean(new CorsFilter(source)); bean.setOrder(0); return bean; } @Override protected void configure(HttpSecurity http) throws Exception { http.headers().cacheControl(); http.csrf().disable() .authorizeRequests() .anyRequest().authenticated() .and() .httpBasic().disable() .addFilterBefore(new JWTLoginFilter("/login", authenticationManager(), tokenService), UsernamePasswordAuthenticationFilter.class) .addFilterBefore(new JWTAuthenticationFilter(tokenService), UsernamePasswordAuthenticationFilter.class); } } |
This class’s authenticationManager()
method has been overridden by our custom authentication AuthenticationProviderImpl.java . There are two filters (JWTLoginFilter, JWTAuthenticationFilter) were added for logging in and another for authenticating the existing JWT token in request. So all we did was disable default basic http security of spring and made all the resources to be available for only authenticated user and rest all are handled by Spring boot security.
JWTAuthenticationFilter.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
public class JWTAuthenticationFilter extends GenericFilterBean { private TokenAuthenticationService service; public JWTAuthenticationFilter(TokenAuthenticationService service) { this.service = service; } @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) throws IOException, ServletException { Authentication authentication = service.getAuthentication((HttpServletRequest) request); SecurityContextHolder.getContext().setAuthentication(authentication); filterChain.doFilter(request, response); } } |
JWTLoginFilter.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
public class JWTLoginFilter extends AbstractAuthenticationProcessingFilter { private TokenAuthenticationService tokenAuthenticationService; public JWTLoginFilter(String url, AuthenticationManager authenticationManager, TokenAuthenticationService service) { super(new AntPathRequestMatcher(url)); setAuthenticationManager(authenticationManager); tokenAuthenticationService = service; } @Override public Authentication attemptAuthentication(HttpServletRequest httpServletRequest, HttpServletResponse hsr1) throws AuthenticationException, IOException, ServletException { SessionUser credentials = new ObjectMapper().readValue(httpServletRequest.getInputStream(), SessionUser.class); UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(credentials.getUsername(), credentials.getPassword()); return getAuthenticationManager().authenticate(token); } @Override protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authentication) throws IOException, ServletException { AuthenticationTokenImpl auth = (AuthenticationTokenImpl) authentication; tokenAuthenticationService.addAuthentication(response, auth); } } |
We are all good with the security stuffs . In these two filters we are actually calling the TokenAuthenticationService to validate the user’s request token . Now your application is already JWT compatible . Lets create a very simple resource where user can see some restricted stuffs once they are authenticated.
UserResource.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
@RestController @RequestMapping("/user") public class UserResource { @Autowired private RedisService service; @RequestMapping(method = RequestMethod.GET) public String getName(AccessTokenBasedAuth auth, HttpServletResponse response) { return auth.getPrincipal().toString(); } @RequestMapping(value = "/processor", method = RequestMethod.GET) public Integer getProcessor(AccessTokenBasedAuth auth, HttpServletResponse response) { return Runtime.getRuntime().availableProcessors(); } @RequestMapping(value = "/logout", method = RequestMethod.GET) public String logout(AccessTokenBasedAuth auth, HttpServletResponse response) { service.setValue(auth.getPrincipal().toString().toLowerCase(), ""); return "Logout Successfully"; } } |
This class contains some endpoints which is accessible using specified uri pattern and method type in @RequestMapping
. Lets try to do some real tests with this minimal application that we have created.
5. Testing
i) Login
ii) Request resource using JWT token
6. Source code
For the source code it is available in github repository : spring-boot-redis-jwt
Please feel free to share your thoughts on this post by commenting below 🙂 Thanks
How do you register the users so they can log in?
No, there are no users right now . As i have mentioned in the blog post that this is just a user authentication based upon username=password . Once it is authenticated it will be saved in redis.
Thanks
First all of all I’m really grateful for you post.
I have a question. When I configured redis configuration class, i took a warning like these;
“The method setHostName(String) from the type JedisConnectionFactory is deprecated”
or
“The method setPort(int) from the type JedisConnectionFactory is deprecated”
I know that there would work, but what is the true configuration for Jedis?
Maybe with this?
https://docs.spring.io/spring-data/redis/docs/current/api/org/springframework/data/redis/connection/RedisStandaloneConfiguration.html
Where is defined the class RedisService.java?
Sorry, found it.
Hi Alper,
It is because those method will not be supported in future and you are right that RedisStandloneConfiguration class is going to be used in future. To implement those configuration it should be something like this.
Thanks
ur example not working….gj
Hi alex,
I found the issue it is because of ENC_KEY environment variable. I would instead make the variable in application.properties , I have pushed a changes to the repository https://github.com/privatejava/spring-boot-redis-jwt . You can checkout and please let me know.
Thanks
Narayan
wow….now its working)
if i want different users then how can i proceed further
Currently i have used username==password authentication but if you want to make it dynamic you might want to change
AuthenticationProviderImpl
class’sauthenticate()
I want to have some public API’s also.
Then what changes we need.
how do login using postman
My login API is login_new, which does DB authentication. I want to generate JWT token only when DB authentication is successful. How to achieve that?