DEV Community

Cover image for Decrypt & Validate JWE Tokens with Spring Security
Florian Storz for devlix Blog

Posted on • Originally published at devlix.de

Decrypt & Validate JWE Tokens with Spring Security

All systems that are exposed to a flood of requests must respond in a short time due to today's requirements. It is therefore important that the individual steps for processing a request are as fast and secure as possible. Therefore, stateless protocols such as OAuth 2.0 (or OIDC) are used for authentication purposes, for example. This protocol usually uses so-called JWTs (JSON Web Token). These tokens contain data about the user and his permissions in the system. The information is usually readable by anyone who has access to the communication and also by anyone involved in the connection between the user and the system (e.g. browser, proxy server or reverse proxy).

Usually, this is referred to as JWS (JSON Web Signature) tokens. To ensure that a potential change can be detected during transmission, these tokens are normally secured against this by signatures (immutability). In addition, there is another form of JWT, the JWE (JSON Web Encryption) tokens, which transmit the content in encrypted form (confidentiality). The encryption does not ensure who created the data. Therefore, when using JWE tokens, a JWS token is normally used as content to ensure the necessary security.

What are JWE Tokens in a nutshell?

JWE tokens are basically a standardized way of exchanging structured data between systems in encrypted form. The RFC standard offers a wide variety of symmetrical and asymmetrical methods for encryption. The list of supported encryption algorithms can be found in the RFC-7518. The payload to be transmitted contains a JWS token for stronger security, which ensures the immutability and correctness of the data.

What is the structure of JWS and JWE tokens?

A JWS token consists of three parts separated by a dot (.) - {header}.{payload}.{signature}. The header contains, among other things, information about the algorithm used to create the signature and how it can be verified. The payload contains the data to be transmitted, and the last part contains the digital signature.

A JWE token consists of five parts separated by a dot (.) - {Header}.{EncryptionKey}.{InitializationVector}.{Ciphertext}.{AuthenticationTag}. The header contains, among other things, information about which algorithm was used to encrypt the data and whether there is a JWS in the data. The two parts EncryptionKey and InitializationVector contain technical data relevant to decrypting the data. The Ciphertext contains the encrypted data. The AuthenticationTag is used to ensure that the token has not been altered.

If you would like to know more about JWS and JWE tokens, you can read this interesting article about it or look at the respective RFC specifications.

How can JWE tokens be decrypted and verified with Spring Boot or Spring Security?

Spring Boot in combination with Spring Security includes the functionality to include the verification of JWS and JWE tokens easily in the authentication process of the system.

Including the following dependency (e.g. via Maven) adds the functionality to the system.

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>
Enter fullscreen mode Exit fullscreen mode

Who is interested: The configuration in the Spring Boot Framework is done in the following classes.

  • org.springframework.boot.autoconfigure.security.oauth2.resource.servlet.OAuth2ResourceServerJwtConfiguration.JwtDecoderConfiguration:jwtDecoderByJwkKeySetUri
  • org.springframework.boot.autoconfigure.security.oauth2.resource.servlet.OAuth2ResourceServerJwtConfiguration.OAuth2SecurityFilterChainConfiguration

Including the JWS Token Check in Spring Boot

The following Spring Boot configuration shows how to verify the tokens:

JwsSecurityConfiguration.java
@Configuration
@EnableWebSecurity
public class JwsSecurityConfiguration {}
Enter fullscreen mode Exit fullscreen mode
application.yaml
spring:
  security:
    oauth2:
      resourceserver:
        jwt:
          jwk-set-uri: ${url}/.well-known/jwks.json
          issuer-uri: ${url}
          audiences: resource-server-app
Enter fullscreen mode Exit fullscreen mode

Including the JWE Token Check in Spring Boot

To include the verification of JWE tokens, the following configuration can be used. Among other things, this requires a private key for decryption. In this example it is a RSA private key. Because of that, it must be defined for which algorithms the provided private key can be used for decryption. This configuration is largely based on an example project of the Spring Security Framework, which can be found here.

JwtDecoderConfiguration.java
@Configuration
@EnableWebSecurity
public class JwtDecoderConfiguration {
    private final JWEAlgorithm jweAlgorithm = JWEAlgorithm.RSA_OAEP_256;
    private final EncryptionMethod encryptionMethod = EncryptionMethod.A256GCM;
    private final OAuth2ResourceServerProperties.Jwt properties;
    private final RSAPrivateKey key;

    JwtDecoderConfiguration(OAuth2ResourceServerProperties properties,
                            @Value("${sample.jwe-key-value}") RSAPrivateKey key) {
        this.properties = properties.getJwt();
        this.key = key;
    }

    @Bean
    JwtDecoder jwtDecoderByJwkKeySetUri() {
        NimbusJwtDecoder nimbusJwtDecoder = NimbusJwtDecoder.withJwkSetUri(this.properties.getJwkSetUri())
                .jwsAlgorithms(this::jwsAlgorithms)
                .jwtProcessorCustomizer(this::jwtProcessorCustomizer)
                .build();
        String issuerUri = this.properties.getIssuerUri();
        OAuth2TokenValidator<Jwt> defaultValidator = JwtValidators.createDefaultWithIssuer(issuerUri);
        nimbusJwtDecoder.setJwtValidator(getValidators(defaultValidator));
        return nimbusJwtDecoder;
    }

    private void jwsAlgorithms(Set<SignatureAlgorithm> signatureAlgorithms) {
        for (String algorithm : this.properties.getJwsAlgorithms()) {
            signatureAlgorithms.add(SignatureAlgorithm.from(algorithm));
        }
    }

    private OAuth2TokenValidator<Jwt> getValidators(OAuth2TokenValidator<Jwt> defaultValidator) {
        List<String> audiences = this.properties.getAudiences();
        List<OAuth2TokenValidator<Jwt>> validators = new ArrayList<>();
        validators.add(defaultValidator);
        validators.add(new JwtClaimValidator<List<String>>(JwtClaimNames.AUD,
                (aud) -> aud != null && !Collections.disjoint(aud, audiences)));
        return new DelegatingOAuth2TokenValidator<>(validators);
    }

    private void jwtProcessorCustomizer(ConfigurableJWTProcessor<SecurityContext> jwtProcessor) {
        JWKSource<SecurityContext> jweJwkSource = new ImmutableJWKSet<>(new JWKSet(rsaKey()));
        JWEKeySelector<SecurityContext> jweKeySelector = new JWEDecryptionKeySelector<>(this.jweAlgorithm,
                this.encryptionMethod, jweJwkSource);

        jwtProcessor.setJWEKeySelector(jweKeySelector);
    }

    private RSAKey rsaKey() {
        RSAPrivateCrtKey crtKey = (RSAPrivateCrtKey) this.key;
        Base64URL n = Base64URL.encode(crtKey.getModulus());
        Base64URL e = Base64URL.encode(crtKey.getPublicExponent());
        return new RSAKey.Builder(n, e)
                .privateKey(this.key)
                .keyUse(KeyUse.ENCRYPTION)
                .build();
    }
}
Enter fullscreen mode Exit fullscreen mode
application.yaml
spring:
  security:
    oauth2:
      resourceserver:
        jwt:
          jwk-set-uri: ${mockwebserver.url}/.well-known/jwks.json
          issuer-uri: ${mockwebserver.url}
          audiences: resource-server-app
sample:
  jwe-key-value: classpath:private.key
Enter fullscreen mode Exit fullscreen mode
private.key
-----BEGIN PRIVATE KEY-----
...
-----END PRIVATE KEY-----
Enter fullscreen mode Exit fullscreen mode

Testing the authentication with JWE Tokens

To test the configuration for authentication, valid tokens must be created in the tests. These JWE tokens can be created with the following class. The contents etc. must be adapted to the respective requirements.

@Component
public class TokenService {
    private final JWEAlgorithm jweAlgorithm = RSA_OAEP_256;
    private final EncryptionMethod encryptionMethod = A256GCM;
    private final JWSAlgorithm jwsAlgorithm = RS256;
    private final OAuth2ResourceServerProperties.Jwt properties;
    private final RSAPrivateKey key;

    TokenService(OAuth2ResourceServerProperties properties,
                            @Value("${sample.jwe-key-value}") RSAPrivateKey key) {
        this.properties = properties.getJwt();
        this.key = key;
    }

    public String buildToken() throws Exception {
        SignedJWT signedJWT = new SignedJWT(new JWSHeader.Builder(jwsAlgorithm).build(), new JWTClaimsSet.Builder()
                .issuer(properties.getIssuerUri())
                .audience(properties.getAudiences())
                .subject("subject")
                .issueTime(new Date())
                .expirationTime(new Date(new Date().getTime() + 5000))
                .build());
        signedJWT.sign(new RSASSASigner(rsaSigningKey()));

        JWEObject jweObject = new JWEObject(
                new JWEHeader.Builder(jweAlgorithm, encryptionMethod)
                        .contentType("JWT")
                        .build(),
                new Payload(signedJWT));

        RSAEncrypter encrypter = new RSAEncrypter(rsaEncryptionKey());
        jweObject.encrypt(encrypter);

        return jweObject.serialize();
    }

    private RSAKey rsaEncryptionKey() {
        RSAPrivateCrtKey crtKey = (RSAPrivateCrtKey) this.key;
        Base64URL n = Base64URL.encode(crtKey.getModulus());
        Base64URL e = Base64URL.encode(crtKey.getPublicExponent());
        return new RSAKey.Builder(n, e)
                .keyUse(KeyUse.ENCRYPTION)
                .build();
    }

    private RSAKey rsaSigningKey() {
        RSAPrivateCrtKey crtKey = (RSAPrivateCrtKey) this.key;
        Base64URL n = Base64URL.encode(crtKey.getModulus());
        Base64URL e = Base64URL.encode(crtKey.getPublicExponent());
        return new RSAKey.Builder(n, e)
                .privateKey(crtKey)
                .keyUse(KeyUse.SIGNATURE)
                .build();
    }
}
Enter fullscreen mode Exit fullscreen mode

Summary

Based on our experience JWE tokens are not often used. Therefore, information regarding the usage in frameworks like Spring Boot and Spring Security is very rare. With this blog post we would like to make things easier for other developers who have to work with JWE Tokens and Spring Boot. Have fun 😃


Florian Storz is writing for the devlix Blog at https://www.devlix.de/blog
This article was published first here (german): https://www.devlix.de/jwe-tokens-mit-spring-security-entschluesseln-und-verifizieren/

devlix logo

devlix GmbH: quality, consulting, development

Top comments (1)

Collapse
 
ygrek profile image
maciej ygrek

Hello, for everyone that are looking how to authorize requests to resource server with an access token that is a JWE, and (required) your oauth2 server has an introspection endpoint exposed (this is a standard, so it should be - RFC-7662), spring has a way to transparently exchange the encoded token to the user Principal - so no need for a manual decoding.

It all get's down just to providing config properties:

spring.security.oauth2.resourceserver.opaque-token:
          introspection-uri: 
          client-id: 
          client-secret: 
Enter fullscreen mode Exit fullscreen mode

And in your security @Configuration

http..oauth2ResourceServer(rs -> rs.opaqueToken(Customizer.withDefaults()));
Enter fullscreen mode Exit fullscreen mode

I'm using spring-boot-starter-oauth2-resource-server:3.1.2

This is described in Spring docs