0

I got the task to implement jwks on the project. On our project, we have implemented a token validation check with oauth2. We use a jks format certificate to obtain a public key. the private key is not used in our project, since we need to check the validity of the token. Our goal is to get rid of the .jks file. There are too few resources for jwks and therefore some points are not clear. If I understand correctly, then jwks mean that there is a jwks.json file in the resources with keys inside, which we select by kid from the token header. Based on the documentation, it is not clear what kind of file it is and how it is loaded for checking by kid, that is, at what moment it happens.Does anyone have a project that can be used as an example? thanks in advance

https://docs.spring.io/spring-security-oauth2-boot/docs/2.2.x-SNAPSHOT/reference/html/boot-features-security-oauth2-authorization-server.html

2 Answers2

2

You can use spring-boot resource server implementation.

First, what you need is to add the following dependency to your project

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

Second, you need to add an authentication server configuration. The JSON file that you mentioned has to be located on the authentication server or you can use JWKs URL of the authentication server. You should have a configuration in your properties file like this.

spring.security.oauth2.resourceserver.jwt.jwk-set-uri=https:/example.com/.well-known/openid-configuration/jwks
spring.security.oauth2.resourceserver.jwt.issuer-uri=https:/example.com

Finally, you need to follow the natural spring-security API configuration. What you need is like the following.

@Configuration
@EnableWebSecurity
public class SecureSecurityConfiguration extends WebSecurityConfigurerAdapter {

    @Value("${spring.security.oauth2.resourceserver.jwt.jwk-set-uri}")
    private String jwtSetUri;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.requiresChannel().anyRequest().requiresInsecure().and().cors()
                .and().csrf().disable()
                .authorizeRequests()
                .antMatchers(HttpMethod.GET, "some path1").permitAll()
                .antMatchers(HttpMethod.POST, "some path2").permitAll()
                .antMatchers(HttpMethod.GET, "some path3").permitAll()
                .antMatchers("/**").hasAuthority("some scope") // if you need this scope.
                .anyRequest()
                .authenticated()
                .and()
                .oauth2ResourceServer()
                .jwt().decoder(jwtDecoder());
    }


    @Bean
    CorsConfigurationSource corsConfigurationSource() {
        final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        CorsConfiguration config = new CorsConfiguration().applyPermitDefaultValues();
        config.addAllowedMethod("PUT");
        config.addAllowedMethod("DELETE");
        source.registerCorsConfiguration("/**", config);
        return source;
    }

    private JwtDecoder jwtDecoder() {
        return NimbusJwtDecoder.withJwkSetUri(jwtSetUri)
                .jwtProcessorCustomizer(p -> p.setJWSTypeVerifier(
                        new DefaultJOSEObjectTypeVerifier<>(new JOSEObjectType("at+jwt")))).build();
    }
}

After this, each request to your APIs should be verified automatically by the Spring by using the authentication server.

Dharman
  • 30,962
  • 25
  • 85
  • 135
Adil Karaöz
  • 216
  • 1
  • 11
0

If you have a JWKS URL, that is basically sets of keys in a JSON response, the following code might help to implement JWKS validation of a JWT access token.

package com.example.project;

import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.configurationprocessor.json.JSONArray;
import org.springframework.boot.configurationprocessor.json.JSONException;
import org.springframework.boot.configurationprocessor.json.JSONObject;
import org.springframework.stereotype.Component;

import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.math.BigInteger;
import java.net.URL;
import java.net.URLConnection;
import java.security.KeyFactory;
import java.security.NoSuchAlgorithmException;
import java.security.PublicKey;
import java.security.Signature;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.RSAPublicKeySpec;
import java.util.Base64;

import static java.nio.charset.StandardCharsets.UTF_8;


@Component
public class JWKSValidation {

    @Value("${jwks.url}")
    private String jwksURL;

    private static PublicKey getPublicKey(String modulus, String exponent) {
        try {
            byte[] exponentB = Base64.getUrlDecoder().decode(exponent);
            byte[] modulusB = Base64.getUrlDecoder().decode(modulus);
            BigInteger bigExponent = new BigInteger(1, exponentB);
            BigInteger bigModulus = new BigInteger(1, modulusB);

            PublicKey publicKey;
            publicKey = KeyFactory.getInstance("RSA").generatePublic(new RSAPublicKeySpec(bigModulus, bigExponent));

            return publicKey;

        } catch (InvalidKeySpecException | NoSuchAlgorithmException e) {
            throw new RuntimeException(e);
        }
    }

    public JSONObject decodeJWT(String jwtToken) throws JSONException {
        String[] split_string = jwtToken.split("\\.");
        String base64EncodedBody = split_string[0];
        Base64.Decoder decoder = Base64.getDecoder();
        byte[] bytes = decoder.decode(base64EncodedBody);
        String decodedString = new String(bytes, UTF_8);

        return new JSONObject(decodedString);

    }

    private JsonObject jwksResponse() throws IOException {
        URL url = new URL(jwksURL);
        URLConnection request = url.openConnection();
        request.connect();
        JsonElement response = JsonParser.parseReader(new InputStreamReader((InputStream) request.getContent()));

        return response.getAsJsonObject();
    }

    private String signedData(String jwtToken) {
        return jwtToken.substring(0, jwtToken.lastIndexOf("."));
    }

    private byte[] signature(String jwtToken) {
        String signatureB64u = jwtToken.substring(jwtToken.lastIndexOf(".") + 1);
        return Base64.getUrlDecoder().decode(signatureB64u);
    }

    public boolean verifySignature(String jwtToken, String kid) throws Exception {
        JsonObject jwkresponse = jwksResponse();
        String jsonString = jwkresponse.toString();

        JSONObject jwks = new JSONObject(jsonString);
        JSONArray jsonArray = jwks.getJSONArray("keys");

        String modulus = null, exponent = null;

        for (int i = 0; i < jsonArray.length(); i++) {
            JSONObject result = jsonArray.getJSONObject(i);

            if (result.getString("kid").equals(kid)) {
                modulus = result.getString("n");
                exponent = result.getString("e");
            }
        }

        PublicKey publicKey = getPublicKey(modulus, exponent);
        Signature signature = Signature.getInstance("SHA256withRSA");
        signature.initVerify(publicKey);
        String signatureData = signedData(jwtToken);
        byte[] signatures = signature(jwtToken);
        signature.update(signatureData.getBytes());

        return signature.verify(signatures);
    }
}

You can autowire the above class and pass the JWT access token in the verifySignature method and the kid of the access token and works in those algorithm whose JWT header is RSA based. And the JWKS URL should be mentioned in application.properties file. Similar approach might be implemented for other algorithms.

parad0X
  • 11
  • 3