Skip to content

Instantly share code, notes, and snippets.

@deependhamecha
Last active July 6, 2022 11:21
Show Gist options
  • Save deependhamecha/aa982dc06924335998a5e0983beff29c to your computer and use it in GitHub Desktop.
Save deependhamecha/aa982dc06924335998a5e0983beff29c to your computer and use it in GitHub Desktop.
Spring-Security + JWT + REST

Spring Security + JWT + REST API

We will create a sample application for JWT + Stateless Token + Spring Security + REST(JSON) API

  1. Create a Spring Boot Project with following dependencies:
    • Spring Web
    • Spring Security
    • Spring Boot DevTools and Other dependencies
<dependency>
    <groupId>javax.xml.bind</groupId>
    <artifactId>jaxb-api</artifactId>
    <version>2.3.1</version>
</dependency>

<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.9.1</version>
</dependency>
  1. Create packages

    • controller
    • filter
    • services
    • model
    • utility
  2. Implement a UserDetailsService:

package com.example.demo.service;

import java.util.ArrayList;

import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

@Service
public class UserService implements UserDetailsService {

	@Override
	public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {

		// Logic to get user from database
		// for now just creating a dummy user

		return new User("admin", "password", new ArrayList<>()); // ArrayList is for Roles
	}

	
}
  1. Then create a SecurityConfiguration for Spring Security:
package com.example.demo.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
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.config.http.SessionCreationPolicy;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

import com.example.demo.filter.JwtFilter;
import com.example.demo.service.UserService;



@Configuration
@EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {

	@Autowired
	private UserService userService;

	@Override
	protected void configure(AuthenticationManagerBuilder auth) throws Exception {

		auth.userDetailsService(this.userService);
	}

    // Need during authentication when user gives username and password
	@Override
	@Bean
	protected AuthenticationManager authenticationManager() throws Exception {
		return super.authenticationManager();
	}

	@Bean
	@SuppressWarnings("deprecation")
	public PasswordEncoder passwordEncoder() {
		return MyNoOpPasswordEncoder.getInstance();
	}
}

Create MyNoOpPasswordEncoder.java file in utility package as NoOpPasswordEncoder is deprecated;

package com.example.demo.utility;

import org.springframework.security.crypto.password.PasswordEncoder;

public final class MyNoOpPasswordEncoder implements PasswordEncoder {

	private static final PasswordEncoder INSTANCE = new MyNoOpPasswordEncoder();

	private MyNoOpPasswordEncoder() {
	}

	@Override
	public String encode(CharSequence rawPassword) {
		return rawPassword.toString();
	}

	@Override
	public boolean matches(CharSequence rawPassword, String encodedPassword) {
		return rawPassword.toString().equals(encodedPassword);
	}

	/**
	 * Get the singleton {@link NoOpPasswordEncoder}.
	 */
	public static PasswordEncoder getInstance() {
		return INSTANCE;
	}

}

Override an overloaded configure method with Param AuthenticationManagerBuilder and pass UserDetailsService.

  1. Create a Controller
@GetMapping("/")
public String home() {
    return "Welcome";
}
	
  1. Create a JWT Utility class for methods:
package com.example.demo.utility;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;

import java.io.Serializable;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Function;

@Component
public class JWTUtility implements Serializable {

    private static final long serialVersionUID = 234234523523L;

    public static final long JWT_TOKEN_VALIDITY = 5 * 60 * 60;

    @Value("${jwt.secret}")
    private String secretKey;

    //retrieve username from jwt token
    public String getUsernameFromToken(String token) {
        return getClaimFromToken(token, Claims::getSubject);
    }

    //retrieve expiration date from jwt token
    public Date getExpirationDateFromToken(String token) {
        return getClaimFromToken(token, Claims::getExpiration);
    }


    public <T> T getClaimFromToken(String token, Function<Claims, T> claimsResolver) {
        final Claims claims = getAllClaimsFromToken(token);
        return claimsResolver.apply(claims);
    }


    //for retrieving any information from token we will need the secret key
    private Claims getAllClaimsFromToken(String token) {
        return Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token).getBody();
    }


    //check if the token has expired
    private Boolean isTokenExpired(String token) {
        final Date expiration = getExpirationDateFromToken(token);
        return expiration.before(new Date());
    }


    //generate token for user
    public String generateToken(UserDetails userDetails) {
        Map<String, Object> claims = new HashMap<>();
        return doGenerateToken(claims, userDetails.getUsername());
    }


    //while creating the token -
    //1. Define  claims of the token, like Issuer, Expiration, Subject, and the ID
    //2. Sign the JWT using the HS512 algorithm and secret key.
    private String doGenerateToken(Map<String, Object> claims, String subject) {
        return Jwts.builder().setClaims(claims).setSubject(subject).setIssuedAt(new Date(System.currentTimeMillis()))
                .setExpiration(new Date(System.currentTimeMillis() + JWT_TOKEN_VALIDITY * 1000))
                .signWith(SignatureAlgorithm.HS512, secretKey).compact();
    }


    //validate token
    public Boolean validateToken(String token, UserDetails userDetails) {
        final String username = getUsernameFromToken(token);
        return (username.equals(userDetails.getUsername()) && !isTokenExpired(token));
    }
}
  1. Create JWT DTO Request and Response:
package com.example.demo.model;

public class JwtRequest {

	private String username;
	private String password;
	public String getUsername() {
		return username;
	}
	public void setUsername(String username) {
		this.username = username;
	}
	public String getPassword() {
		return password;
	}
	public void setPassword(String password) {
		this.password = password;
	}

	public JwtRequest(String username, String password) {
		super();
		this.username = username;
		this.password = password;
	}

	public JwtRequest() {
		super();

	}
}
package com.example.demo.model;

public class JwtResponse {

	private String jwtToken;

	public String getJwtToken() {
		return jwtToken;
	}

	public void setJwtToken(String jwtToken) {
		this.jwtToken = jwtToken;
	}

	public JwtResponse(String jwtToken) {
		super();
		this.jwtToken = jwtToken;
	}

	public JwtResponse() {
		super();
		// TODO Auto-generated constructor stub
	}
}
  1. Add Controller to Authenticate
@PostMapping("/authenticate")
public JwtResponse authenticate(@RequestBody JwtRequest jwtRequest) throws Exception {
    try {
        authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(jwtRequest.getUsername(), jwtRequest.getPassword()));
    } catch(BadCredentialsException e) {
        throw new Exception("INVALID CREDENTIALS", e);
    }
    
    // If it succeeds then
    final UserDetails userDetails = userService.loadUserByUsername(jwtRequest.getUsername());
    
    // generate token
    final String token = jwtUtility.generateToken(userDetails);
    
    return new JwtResponse(token);
}
  1. Add Routes in SecurityConfig:
@Override
protected void configure(HttpSecurity http) throws Exception {
    http
        .csrf()
        .disable()
        .authorizeRequests()
        .antMatchers("/authenticate")
        .permitAll()
        .anyRequest()
        .authenticated();
}
  1. Specify jwt.secret in application.properties.

Now, try hitting the url /authenticate in postman with POST request and body as raw JSON.

{
    "username": "admin",
    "password": "password"
}
  1. Now, we will create filter for checking for Authorization Header, its token and a valid user from token.
package com.example.demo.filter;

import java.io.IOException;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;

import com.example.demo.service.UserService;
import com.example.demo.utility.JWTUtility;

@Component
public class JwtFilter extends OncePerRequestFilter {

	private String TOKEN_PREFIX = "Bearer ";

	@Autowired
	private JWTUtility jwtUtility;

	@Autowired
	private UserService userService;

	@Override
	protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
			throws ServletException, IOException {

		// Check for token in header
		String authorization = request.getHeader("Authorization");
		String token = null;
		String username = null;
		
		// Check if Header has Bearer and extract the token and get user from token
		if(authorization != null && authorization.startsWith(TOKEN_PREFIX)) {
			token = authorization.substring(7);
			username = jwtUtility.getUsernameFromToken(token);
		}
		
		// Check if valid user or not
		if(username != null &&
				// SecurityContext should be null
				SecurityContextHolder.getContext().getAuthentication() == null
		) {
			UserDetails userDetails = userService.loadUserByUsername(username);
			
			// Add this to security context
			if(jwtUtility.validateToken(token, userDetails)) {
				UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
				

                // Seriously, dont know what this one is about
				usernamePasswordAuthenticationToken.setDetails(
					new WebAuthenticationDetailsSource().buildDetails(request)
				);
				
				SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);
			}
		}
		
		// Continue to the next filter
		filterChain.doFilter(request, response);
	}

}

Remember until now spring security is using session behaviour, you need to tell spring security to not to use it check SecurityConfig sessionmanagementblock.

http
    .csrf()
    .disable()
    .authorizeRequests()
    .antMatchers("/authenticate")
    .permitAll()
    .anyRequest()
    .authenticated()

    .and()
    .sessionManagement()
    .sessionCreationPolicy(SessionCreationPolicy.STATELESS);

    http.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class);

Remember, to add in SecurityConfig.java

@Autowired
private JwtFilter jwtFilter;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment