OAuth 2 client for JWT bearer grant using Spring OAuth
Asked Answered
M

1

6

In the Spring OAUTH library under org.springframework.security.oauth2.client.token.grant package we have grants for client, code, implicit and password.

There are some extension grants like jwt-bearer or SAML which requires assertion to be sent for token generation.

Grant type: urn:ietf:params:oauth:grant-type:jwt-bearer

Does Spring OAUTH 2 support these extension grants. If so which class to use? Do we need to write a custome class to support it ?

Melitamelitopol answered 27/6, 2019 at 1:2 Comment(2)
baeldung.com/spring-security-oauth-jwt is a good quick startArther
Unfortunately the baeldung.com/spring-security-oauth-jwt tutorial is to build an Authorization server, not a client to use it.Laminous
L
0

As a starting point, you can handle the OAuth2 JWT bearer flow manually with the following code which basically creates a filter to configure the WebClient that will connect to the Resource server.

The filter creates the signed JWT you will send to the Authorization server to get back the access token that will be used in the final request header as a Bearer token.

Note that the flow you refer to is defined in the RFC-7523 (art. 2.1) specification.

Here is the filter :


private ExchangeFilterFunction jwtAuthorizationGrantFilter() {
    return (request, next) -> {

        // Create a JWT with the required claims
        String jwtToken = JWT.create()
                .withClaim("iss", "3eeff86e-xxxxx-beafaa5ee66f")
                .withClaim("sub", "a1e757a3-xxxxx-77c5780b9284")
                .withClaim("aud", "account-d.docusign.com")
                .withIssuedAt(Instant.now())
                .withExpiresAt(Instant.now().plus(100, ChronoUnit.MINUTES))
                .withClaim("scope", "signature impersonation")
                .sign(algorithm);


        // Send the JWT to the authorization server and get an access token
        return webClient.post()
                .uri("https://account-d.docusign.com/oauth/token") // Your authorization server token endpoint here
                .accept(MediaType.APPLICATION_JSON)
                .body(BodyInserters.fromFormData("grant_type", "urn:ietf:params:oauth:grant-type:jwt-bearer")
                        .with("assertion", jwtToken))
                .retrieve()
                .onStatus(HttpStatus::isError, response -> {
                    logTraceResponse(log, response);
                    return response.createException().flatMap(Mono::error);
                })
                .bodyToMono(OAuth2AccessTokenResponse.class)
                // Add the access token as a header to the original request
                .map(tokenResponse -> tokenResponse.getAccessToken().getTokenValue())
                .flatMap(token -> next.exchange(ClientRequest.from(request)
                            .header(HttpHeaders.AUTHORIZATION, "Bearer " + token)
                            .build()));
    };
}

The only dependencies needed are the following, with the auth0 one being used to create the JWT above :

implementation("org.springframework.boot:spring-boot-starter-oauth2-client")
implementation("org.springframework.boot:spring-boot-starter-webflux")
implementation("com.auth0:java-jwt:4.4.0")

Then you apply the filter to the WebClient :

private WebClient dsWebClient = WebClient.builder()
        .filter(jwtAuthorizationGrantFilter())
        .build();

public Mono<String> makeApiCall() {
    return dsWebClient.get()
            .uri("https://account-d.docusign.com/oauth/userinfo")
            .retrieve()
            .bodyToMono(String.class);
}

Then, the only missing part is handling the public and private keys to sign the JWT :


    private static final Base64.Decoder DECODER = Base64.getMimeDecoder();

    private static final RSAPublicKey publicKey = getRSAPublicKey();
    private static final RSAPrivateKey privateKey = getRSAPrivateKey();

    private static final Algorithm algorithm = Algorithm.RSA256(publicKey, privateKey);

    private static RSAPrivateKey getRSAPrivateKey() {
        Path privateKeyPath = Paths.get("src/main/resources/certificates/docusignPrivateKey");
        try {
            byte[] privateKeyBytes = buildPkcs8KeyFromPkcs1Key(convertCertFilesToBytes(privateKeyPath));
            PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(privateKeyBytes);
            KeyFactory keyFactory = KeyFactory.getInstance("RSA");
            return (RSAPrivateKey) keyFactory.generatePrivate(spec);
        } catch (IOException | NoSuchAlgorithmException | InvalidKeySpecException e) {
            throw new RuntimeException("Cannot read private key in " + privateKeyPath);
        }
    }

    private static RSAPublicKey getRSAPublicKey() {
        Path privateKeyPath = Paths.get("src/main/resources/certificates/docusignPublicKey");
        try {
            byte[] publicKeyBytes = convertCertFilesToBytes(privateKeyPath);
            X509EncodedKeySpec spec = new X509EncodedKeySpec(publicKeyBytes);
            KeyFactory keyFactory = KeyFactory.getInstance("RSA");
            return (RSAPublicKey) keyFactory.generatePublic(spec);
        } catch (IOException | NoSuchAlgorithmException | InvalidKeySpecException e) {
            throw new RuntimeException("Cannot read private key in " + privateKeyPath);
        }
    }

    private static byte[] buildPkcs8KeyFromPkcs1Key(byte[] innerKey) {
        final byte[] result = new byte[innerKey.length + 26];
        System.arraycopy(DECODER.decode("MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKY="), 0, result, 0, 26);
        System.arraycopy(BigInteger.valueOf(result.length - 4).toByteArray(), 0, result, 2, 2);
        System.arraycopy(BigInteger.valueOf(innerKey.length).toByteArray(), 0, result, 24, 2);
        System.arraycopy(innerKey, 0, result, 26, innerKey.length);
        return result;
    }

    private static byte[] convertCertFilesToBytes(Path certPath) throws IOException {
        String certString = Files.readString(certPath).replaceAll("-----.+KEY-----", "");
        return DECODER.decode(certString);
    }

    public static void logTraceResponse(Logger log, ClientResponse response) {
        log.trace("Response status: {}", response.statusCode());
        log.trace("Response headers: {}", response.headers().asHttpHeaders());
        response.bodyToMono(String.class)
                .publishOn(Schedulers.boundedElastic())
                .subscribe(body -> log.trace("Response body: {}", body));
    }


This solution will probably request an access token at each request.

A more involving solution would use Spring's ReactiveClientRegistrationRepository and ServerOAuth2AuthorizedClientRepository to automatically configure the Client Registration and Authorized Clients but the lack of example/documentation of a Spring Boot CLIENT using this JWT Flow makes creating this enhanced solution a real puzzle.

Laminous answered 25/7, 2023 at 22:53 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.