Implementing JWT Token to Authorise Rest API in Spring Boot without using Spring security
As we are moving towards Micro service based architecture most of our API are required to be state less and adoption of REST API is at peak. so to authorize our request we have one globally accepted method is through JWT. So JWT is Json based web token use to transfers claims securely between two parties. and spring security provides methods and configuration to achieve easily, but when our app need customization we trend to add more boilerplate code and code readability and complexity becomes more deeper, so instead we can focus on what we need and how we need just by writing our custom logic. so in this tutorial I will be using springsandwich library, but there is another way to implement authorization you can pick which ever is convenient to you [Authorizations through custom annotation].
Step 1. Adding required dependency.
- Spring boot web starter package
As we know spring boot web starter would makes easily start web application providing necessary configuration. - Spring Sandwich
This is a custom interceptor library which is far more simpler than spring interceptor or spring aspects you read more on in this link - Jose Jwt plugin
This plugin will help to endoce and decode the jwt token
So dependency maven list looks like this
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.kastkode</groupId>
<artifactId>springsandwich</artifactId>
<version>[1.0.2,)</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
</dependency>
<dependency>
<groupId>org.bitbucket.b_c</groupId>
<artifactId>jose4j</artifactId>
<version>0.6.5</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
Step 2. Let's create the component which will create JWT token and parse JWT token
@Component
public class JWTBuilder {
@Value("${jwt.issuer}")
private String jwtIssuer;
@Value("${jwt.secret}")
private String jwtSecret;
@Value("${jwt.expiry}")
private Float jwtExpiry;
RsaJsonWebKey rsaJsonWebKey;
public JWTBuilder() {
}
public String generateToken(String usersId,String roles) {
try {
JwtClaims jwtClaims = new JwtClaims();
jwtClaims.setIssuer(jwtIssuer);
jwtClaims.setExpirationTimeMinutesInTheFuture(jwtExpiry);
jwtClaims.setAudience("ALL");
jwtClaims.setStringListClaim("groups", roles);
jwtClaims.setGeneratedJwtId();
jwtClaims.setIssuedAtToNow();
jwtClaims.setSubject("AUTHTOKEN");
jwtClaims.setClaim("userId", usersId);
JsonWebSignature jws = new JsonWebSignature();
jws.setPayload(jwtClaims.toJson());
jws.setKey(rsaJsonWebKey.getPrivateKey());
jws.setKeyIdHeaderValue(rsaJsonWebKey.getKeyId());
jws.setAlgorithmHeaderValue(AlgorithmIdentifiers.RSA_USING_SHA256);
return jws.getCompactSerialization();
} catch (JoseException e) {
e.printStackTrace();
return null;
}
}
public JwtClaims generateParseToken(String token) {
JwtConsumer jwtConsumer = new JwtConsumerBuilder()
.setRequireExpirationTime()
.setSkipSignatureVerification()
.setAllowedClockSkewInSeconds(60)
.setRequireSubject()
.setExpectedIssuer(jwtIssuer)
.setExpectedAudience("ALL")
.setExpectedSubject("AUTHTOKEN")
.setVerificationKey(rsaJsonWebKey.getKey())
.setJwsAlgorithmConstraints(
new AlgorithmConstraints(AlgorithmConstraints.ConstraintType.WHITELIST,
AlgorithmIdentifiers.RSA_USING_SHA256))
.build();
try
{
JwtClaims jwtClaims = jwtConsumer.processToClaims(token);
return jwtClaims;
} catch (InvalidJwtException e) {
try {
if (e.hasExpired())
{
throw new AuthException("JWT expired at " + e.getJwtContext().getJwtClaims().getExpirationTime());
}
if (e.hasErrorCode(ErrorCodes.AUDIENCE_INVALID))
{
throw new AuthException("JWT had wrong audience: " + e.getJwtContext().getJwtClaims().getAudience());
}
throw new AuthException(e.getMessage());
} catch (MalformedClaimException innerE) {
throw new AuthException("invalid Token");
}
}
}
@PostConstruct
public void init() {
try {
rsaJsonWebKey = RsaJwkGenerator.generateJwk(2048);
rsaJsonWebKey.setKeyId(jwtSecret);
} catch (JoseException e) {
e.printStackTrace();
}
}
}
basically this component is seperated in three parts first parth is generating the rsaKey
used to encryt the certificate to check its authenticity. and generateToken
is use to create JWT token and generateParseToken
to decode the token and check its authenticity
Step 3. Writing SpringSandwich interceptor to parse the token.
@Component
public class AuthHandler implements BeforeHandler {
Logger logger = LoggerFactory.getLogger(AuthHandler.class);
@Autowired
JWTBuilder jwtbuilder
@Override
public Flow handle(HttpServletRequest request, HttpServletResponse response, HandlerMethod handler, String[] flags) throws Exception {
logger.debug(request.getMethod() + "request is executing on" + request.getURI());
String token = request.getHeader("Authorization");
if( token == null) {
throw new RuntimeException("Auth token is required");
}
JWTclaims claims = jwtbuilder.generateParseToken(token)
request.setAttribute(userId,claims.getClaimValue("userId").toString())
//do the db call and check is the user is still valid, avoided to make tutorial simple.
return Flow.CONTINUE;
}
}
So this Interceptor will read token from header and parse it, if the the token is expired or not valid token exception is thrown and http request is with held.
Step 4. Writing dummy controller to generate token and to authorise the request.
Class Controller {
...
@Autowired
JWTBuilder jwtbuilder;
@GetMapping(path="/login")
@Before( @BeforeElement(ConsoleLogger.class))
public String login() {
return jwtbuilder.generateParseToken("test","admin");
}
@GetMapping(path="/authorise", headers = {"Authorization"})
@Before( @BeforeElement(AuthHandler.class))
public String test() {
...
}
...
}
this controller have two dummy api which is straight forward one generates random token for user, and one parse the token for authenticity of the token.
Step 4. Instruct spring boot use spring to use the custom interceptor in main application class.
@ComponentScan(basePackages = {"com.kastkode.springsandwich.filter", "com.your-app-here.*"})
public class Main { ... }
please note com.your-app-here.*
to replace to your application's package
use your post man to trigger the apis and check the result, this tutorial is very straight forward, by eliminating spring security we can implement custom rules and simplify the flow of login and authorisation, which make code easier.
Also you can read implementing the same using spring security