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.