post-photo

Spring JWT with RSA (asymmetric encryption algorithm)

Introduction

Let’s say we have an “auth server” that signs tokens for us and a “resource server” where we store some pretty sensitive data. We trust the auth server and we want to validate that the JWT we get indeed comes from that trusted auth server. In other words, how can we be sure that the guy who wants to detonate our printers is using a JWT that is from that very auth server?

Use JWT with RS256. No, seriously, that’s it.

We’ve seen tons of tutorials about JWT and how to implement it. What baffled us is that almost none of them have even mentioned that you can use JWT with an asymmetric algorithm like RS256 instead of the “common” HS256.

So in this post, we’ll leave HS256 alone (unlike the rest of the internet). Instead, we’ll show you how to create an application in Spring that signs (and optionally validates) tokens and another one that validates them.

The certs

First, let’s generate our certifications with the following commands (we used MinGW on Windows).

ssh-keygen -t rsa -m PEM
-- enter filename, it was <rsa-key> in my example
 
ssh-keygen -m PKCS8 -e
-- save the output into <rsa-key.x509.public>
 
openssl pkcs8 -topk8 -inform pem -in rsa-key -outform pem -nocrypt -out rsa-key.pkcs8.private
-- again rsa-key is my filename, don't forget to change it :)

Once you’ve executed these commands, keep rsa-key.pkcs8.private (for auth server) and rsa-key.x509.public (for resource server). You can get rid of the other files if you want or keep them for later, you do you.

The auth server

We’ll be using the private cert in this application. First, we have to read it at the application start-up (or you can read it every time you generate a token, you decide).

package com.wanari.jwt.example.authserver.jwt;
 
import com.wanari.jwt.example.authserver.jwt.model.exception.JwtInitializationException;
import com.wanari.jwt.example.authserver.util.Base64Util;
import com.wanari.jwt.example.authserver.util.ResourceUtil;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
 
import javax.annotation.PostConstruct;
import java.io.IOException;
import java.security.Key;
import java.security.KeyFactory;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.spec.EncodedKeySpec;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.util.function.BiFunction;
import java.util.function.Function;
 
@Component
@RequiredArgsConstructor
public class JwtKeyProvider {
 
    private final ResourceUtil resourceUtil;
    private final Base64Util base64Util;
 
    @Getter
    private PrivateKey privateKey;
 
    @PostConstruct
    public void init() {
        privateKey = readKey(
            "classpath:keys/rsa-key.pkcs8.private",
            "PRIVATE",
            this::privateKeySpec,
            this::privateKeyGenerator
        );
    }
 
    private <T extends Key> T readKey(String resourcePath, String headerSpec, Function<String, EncodedKeySpec> keySpec, BiFunction<KeyFactory, EncodedKeySpec, T> keyGenerator) {
        try {
            String keyString = resourceUtil.asString(resourcePath);
            //TODO you can check the headers and throw an exception here if you want
 
            keyString = keyString.replace("-----BEGIN " + headerSpec + " KEY-----", "");
            keyString = keyString.replace("-----END " + headerSpec + " KEY-----", "");
            keyString = keyString.replaceAll("\\s+", "");
 
            return keyGenerator.apply(KeyFactory.getInstance("RSA"), keySpec.apply(keyString));
        } catch(NoSuchAlgorithmException | IOException e) {
            throw new JwtInitializationException(e);
        }
    }
 
    private EncodedKeySpec privateKeySpec(String data) {
        return new PKCS8EncodedKeySpec(base64Util.decode(data));
    }
 
    private PrivateKey privateKeyGenerator(KeyFactory kf, EncodedKeySpec spec) {
        try {
            return kf.generatePrivate(spec);
        } catch(InvalidKeySpecException e) {
            throw new JwtInitializationException(e);
        }
    }
 
}

Yeah, I know, the readKey function seems a bit weird. But trust us, if we use both private and public keys in a project (for example, when we want to validate the JWTs in our auth app), it saves us from writing a lot of boilerplate codes.

Alright, thanks to this little provider, our private key is good and ready!

The next step is to implement a service which generates JWTs for us.

package com.wanari.jwt.example.authserver.jwt;
 
import com.wanari.jwt.example.authserver.config.JwtConfig;
import com.wanari.jwt.example.authserver.util.DateUtil;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
 
import java.time.LocalDateTime;
 
@Component
@RequiredArgsConstructor
public class JwtService {
 
    private final JwtConfig jwtConfig;
    private final JwtKeyProvider jwtKeyProvider;
    private final DateUtil dateUtil;
 
    public String generateToken(String username) {
        LocalDateTime now = LocalDateTime.now();
        LocalDateTime expiryDate = now.plusMinutes(jwtConfig.getExpirationInMinutes());
 
        return Jwts.builder()
            .setExpiration(dateUtil.toDate(expiryDate))
            .signWith(SignatureAlgorithm.RS256, jwtKeyProvider.getPrivateKey())
            .claim("username", username)
            .compact();
    }
}

Now we have a function that can generate JWTs. As you can see, we chose to store the username in the claim but, again, it’s up to you.

All we need to do now is to implement an API to generate tokens. We won’t protect this API with any authentication this time but just to be clear: we don’t condone this behavior.

If you have any problems reading the credentials from the request body using SpringSecurity, check this out!

The resource server

Basically, we have our auth server ready to generate tokens for clients at this point. The next step is to implement our resource server so we can validate those tokens with the public key.

Again, let’s start with the provider that can read the public key.

package com.wanari.jwt.example.resourceserver.jwt;
 
import com.wanari.jwt.example.resourceserver.jwt.model.exception.JwtInitializationException;
import com.wanari.jwt.example.resourceserver.util.Base64Util;
import com.wanari.jwt.example.resourceserver.util.ResourceUtil;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
 
import javax.annotation.PostConstruct;
import java.io.IOException;
import java.security.Key;
import java.security.KeyFactory;
import java.security.NoSuchAlgorithmException;
import java.security.PublicKey;
import java.security.spec.EncodedKeySpec;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.X509EncodedKeySpec;
import java.util.function.BiFunction;
import java.util.function.Function;
 
@Component
@RequiredArgsConstructor
public class JwtKeyProvider {
 
    private final ResourceUtil resourceUtil;
    private final Base64Util base64Util;
 
    @Getter
    private PublicKey publicKey;
 
    @PostConstruct
    public void init() {
        publicKey = readKey(
            "classpath:keys/rsa-key.x509.public",
            "PUBLIC",
            this::publicKeySpec,
            this::publicKeyGenerator
        );
    }
 
    private <T extends Key> T readKey(String resourcePath, String headerSpec, Function<String, EncodedKeySpec> keySpec, BiFunction<KeyFactory, EncodedKeySpec, T> keyGenerator) {
        try {
            String keyString = resourceUtil.asString(resourcePath);
            //TODO you can check the headers and throw an exception here if you want
 
            keyString = keyString.replace("-----BEGIN " + headerSpec + " KEY-----", "");
            keyString = keyString.replace("-----END " + headerSpec + " KEY-----", "");
            keyString = keyString.replaceAll("\\s+", "");
 
            return keyGenerator.apply(KeyFactory.getInstance("RSA"), keySpec.apply(keyString));
        } catch(NoSuchAlgorithmException | IOException e) {
            throw new JwtInitializationException(e);
        }
    }
 
    private EncodedKeySpec publicKeySpec(String data) {
        return new X509EncodedKeySpec(base64Util.decode(data));
    }
 
    private PublicKey publicKeyGenerator(KeyFactory kf, EncodedKeySpec spec) {
        try {
            return kf.generatePublic(spec);
        } catch(InvalidKeySpecException e) {
            throw new JwtInitializationException(e);
        }
    }
}

After implementing the key provider, let’s do the same with our JwtService, which will validate the given key (instead of generating keys for us).

package com.wanari.jwt.example.resourceserver.jwt;
 
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtException;
import io.jsonwebtoken.Jwts;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
 
@Component
@Slf4j
@RequiredArgsConstructor
public class JwtService {
 
    private final JwtKeyProvider jwtKeyProvider;
 
    boolean validateToken(String jwt) {
        try {
            Jwts.parser().setSigningKey(jwtKeyProvider.getPublicKey()).parseClaimsJws(jwt);
            return true;
        } catch(JwtException e) {
            log.warn("Invalid JWT!", e);
        }
        return false;
    }
 
    public String getUsernameFrom(String jwt) {
        return (String) getClaims(jwt).get("username");
    }
 
    private Claims getClaims(String jwt) {
        return Jwts.parser()
            .setSigningKey(jwtKeyProvider.getPublicKey())
            .parseClaimsJws(jwt)
            .getBody();
    }
}

Don’t worry, we’ll talk about that getUsernameFrom later (smile)

The last thing you have to do is tell Spring that you want to authenticate users with a JWT and write a little piece of code in the header that extracts data from the JWT if a request is sent to the resource server.

package com.wanari.jwt.example.resourceserver.jwt;
 
import com.wanari.jwt.example.resourceserver.jwt.model.JwtUser;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpHeaders;
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.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;
 
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Collections;
 
@Slf4j
@Component
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends OncePerRequestFilter {
 
    private final JwtService jwtService;
 
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws IOException, ServletException {
        try {
            String jwt = getJwtFromRequest(request);
            if(jwtService.validateToken(jwt)) {
                UserDetails userDetails = JwtUser.builder()
                    .username(jwtService.getUsernameFrom(jwt))
                    .build();
 
                UsernamePasswordAuthenticationToken authentication =
                    new UsernamePasswordAuthenticationToken(userDetails, null, Collections.emptyList());
 
                authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                SecurityContextHolder.getContext().setAuthentication(authentication);
            }
        } catch(Exception e) {
            log.error("Could not set user authentication in security context", e);
        }
 
        filterChain.doFilter(request, response);
    }
 
    private String getJwtFromRequest(HttpServletRequest request) {
        String bearerToken = request.getHeader(HttpHeaders.AUTHORIZATION);
        if(StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
            return bearerToken.substring(7);
        }
        return null;
    }
}

Here’s the mapping between Spring’s user and the given JWT and the reason why we needed that getUsernameFrom function.

package com.wanari.jwt.example.resourceserver.config;
 
import com.wanari.jwt.example.resourceserver.jwt.JwtAuthenticationFilter;
import lombok.AllArgsConstructor;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
 
@Configuration
@AllArgsConstructor
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
 
    private final JwtAuthenticationFilter jwtAuthenticationFilter;
 
    @Override
    protected void configure(final HttpSecurity http) throws Exception {
        http
            .sessionManagement()
            .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            .and()
            .authorizeRequests()
            .anyRequest().authenticated()
            .and()
            .addFilterBefore(
                jwtAuthenticationFilter,
                UsernamePasswordAuthenticationFilter.class
            )
        ;
    }
}

That’s all, folks. Now we have both our auth server and resource server set up.

All we have to do now is call the auth server’s API, prefix the given token with “Bearer” and put it in the Authorization header in case we need to call an API on the resource server.

Head to our GitHub page to download the source code and feel free to play around with it. Just call the resource server’s /secret API with the JWT generated by the auth server and see the magic unfold.

Did you find this tutorial useful? Or do you have any questions or suggestions for how we can do better? Raid the comment section.

Want to learn how to use JWT with RSA in Akka HTTP? Our next post will be right up your alley then. Stay tuned!

member photo

His favorite technologies are AngularJS and Java 8. He's been at Wanari as a full stack developer for almost 3 years.

Latest post by Alex Sükein

Solutions for a filterable sortable pageable list in Spring