Add support for customizing claims in JWT Client Assertion

Closes gh-9855
This commit is contained in:
Steve Riesenberg 2022-03-15 13:40:50 -05:00
parent 4a8219d16c
commit f0168c6c27
No known key found for this signature in database
GPG Key ID: 5F311AB48A55D521
4 changed files with 196 additions and 2 deletions

View File

@ -149,3 +149,35 @@ tokenResponseClient.addParametersConverter(
)
----
====
=== Customizing the JWT assertion
The JWT produced by `NimbusJwtClientAuthenticationParametersConverter` contains the `iss`, `sub`, `aud`, `jti`, `iat` and `exp` claims by default. You can customize the headers and/or claims by providing a `Consumer<NimbusJwtClientAuthenticationParametersConverter.JwtClientAuthenticationContext<T>>` to `setJwtClientAssertionCustomizer()`. The following example shows how to customize claims of the JWT:
====
.Java
[source,java,role="primary"]
----
Function<ClientRegistration, JWK> jwkResolver = ...
NimbusJwtClientAuthenticationParametersConverter<OAuth2ClientCredentialsGrantRequest> converter =
new NimbusJwtClientAuthenticationParametersConverter<>(jwkResolver);
converter.setJwtClientAssertionCustomizer((context) -> {
context.getHeaders().header("custom-header", "header-value");
context.getClaims().claim("custom-claim", "claim-value");
});
----
.Kotlin
[source,kotlin,role="secondary"]
----
val jwkResolver = ...
val converter: NimbusJwtClientAuthenticationParametersConverter<OAuth2ClientCredentialsGrantRequest> =
NimbusJwtClientAuthenticationParametersConverter(jwkResolver)
converter.setJwtClientAssertionCustomizer { context ->
context.headers.header("custom-header", "header-value")
context.claims.claim("custom-claim", "claim-value")
}
----
====

View File

@ -163,3 +163,35 @@ val tokenResponseClient = DefaultClientCredentialsTokenResponseClient()
tokenResponseClient.setRequestEntityConverter(requestEntityConverter)
----
====
=== Customizing the JWT assertion
The JWT produced by `NimbusJwtClientAuthenticationParametersConverter` contains the `iss`, `sub`, `aud`, `jti`, `iat` and `exp` claims by default. You can customize the headers and/or claims by providing a `Consumer<NimbusJwtClientAuthenticationParametersConverter.JwtClientAuthenticationContext<T>>` to `setJwtClientAssertionCustomizer()`. The following example shows how to customize claims of the JWT:
====
.Java
[source,java,role="primary"]
----
Function<ClientRegistration, JWK> jwkResolver = ...
NimbusJwtClientAuthenticationParametersConverter<OAuth2ClientCredentialsGrantRequest> converter =
new NimbusJwtClientAuthenticationParametersConverter<>(jwkResolver);
converter.setJwtClientAssertionCustomizer((context) -> {
context.getHeaders().header("custom-header", "header-value");
context.getClaims().claim("custom-claim", "claim-value");
});
----
.Kotlin
[source,kotlin,role="secondary"]
----
val jwkResolver = ...
val converter: NimbusJwtClientAuthenticationParametersConverter<OAuth2ClientCredentialsGrantRequest> =
NimbusJwtClientAuthenticationParametersConverter(jwkResolver)
converter.setJwtClientAssertionCustomizer { context ->
context.headers.header("custom-header", "header-value")
context.claims.claim("custom-claim", "claim-value")
}
----
====

View File

@ -1,5 +1,5 @@
/*
* Copyright 2002-2021 the original author or authors.
* Copyright 2002-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -22,6 +22,7 @@ import java.util.Collections;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Consumer;
import java.util.function.Function;
import com.nimbusds.jose.jwk.JWK;
@ -62,6 +63,7 @@ import org.springframework.util.MultiValueMap;
*
* @param <T> the type of {@link AbstractOAuth2AuthorizationGrantRequest}
* @author Joe Grandja
* @author Steve Riesenberg
* @since 5.5
* @see Converter
* @see com.nimbusds.jose.jwk.JWK
@ -87,6 +89,9 @@ public final class NimbusJwtClientAuthenticationParametersConverter<T extends Ab
private final Map<String, JwsEncoderHolder> jwsEncoders = new ConcurrentHashMap<>();
private Consumer<JwtClientAuthenticationContext<T>> jwtClientAssertionCustomizer = (context) -> {
};
/**
* Constructs a {@code NimbusJwtClientAuthenticationParametersConverter} using the
* provided parameters.
@ -142,6 +147,10 @@ public final class NimbusJwtClientAuthenticationParametersConverter<T extends Ab
.expiresAt(expiresAt);
// @formatter:on
JwtClientAuthenticationContext<T> jwtClientAssertionContext = new JwtClientAuthenticationContext<>(
authorizationGrantRequest, headersBuilder, claimsBuilder);
this.jwtClientAssertionCustomizer.accept(jwtClientAssertionContext);
JwsHeader jwsHeader = headersBuilder.build();
JwtClaimsSet jwtClaimsSet = claimsBuilder.build();
@ -189,6 +198,21 @@ public final class NimbusJwtClientAuthenticationParametersConverter<T extends Ab
return jwsAlgorithm;
}
/**
* Sets the {@link Consumer} to be provided the
* {@link JwtClientAuthenticationContext}, which contains the
* {@link JwsHeader.Builder} and {@link JwtClaimsSet.Builder} for further
* customization.
* @param jwtClientAssertionCustomizer the {@link Consumer} to be provided the
* {@link JwtClientAuthenticationContext}
* @since 5.7
*/
public void setJwtClientAssertionCustomizer(
Consumer<JwtClientAuthenticationContext<T>> jwtClientAssertionCustomizer) {
Assert.notNull(jwtClientAssertionCustomizer, "jwtClientAssertionCustomizer cannot be null");
this.jwtClientAssertionCustomizer = jwtClientAssertionCustomizer;
}
private static final class JwsEncoderHolder {
private final JwtEncoder jwsEncoder;
@ -210,4 +234,59 @@ public final class NimbusJwtClientAuthenticationParametersConverter<T extends Ab
}
/**
* A context that holds client authentication-specific state and is used by
* {@link NimbusJwtClientAuthenticationParametersConverter} when attempting to
* customize the JSON Web Token (JWS) client assertion.
*
* @param <T> the type of {@link AbstractOAuth2AuthorizationGrantRequest}
* @since 5.7
*/
public static final class JwtClientAuthenticationContext<T extends AbstractOAuth2AuthorizationGrantRequest> {
private final T authorizationGrantRequest;
private final JwsHeader.Builder headers;
private final JwtClaimsSet.Builder claims;
private JwtClientAuthenticationContext(T authorizationGrantRequest, JwsHeader.Builder headers,
JwtClaimsSet.Builder claims) {
this.authorizationGrantRequest = authorizationGrantRequest;
this.headers = headers;
this.claims = claims;
}
/**
* Returns the {@link AbstractOAuth2AuthorizationGrantRequest authorization grant
* request}.
* @return the {@link AbstractOAuth2AuthorizationGrantRequest authorization grant
* request}
*/
public T getAuthorizationGrantRequest() {
return this.authorizationGrantRequest;
}
/**
* Returns the {@link JwsHeader.Builder} to be used to customize headers of the
* JSON Web Token (JWS).
* @return the {@link JwsHeader.Builder} to be used to customize headers of the
* JSON Web Token (JWS)
*/
public JwsHeader.Builder getHeaders() {
return this.headers;
}
/**
* Returns the {@link JwtClaimsSet.Builder} to be used to customize claims of the
* JSON Web Token (JWS).
* @return the {@link JwtClaimsSet.Builder} to be used to customize claims of the
* JSON Web Token (JWS)
*/
public JwtClaimsSet.Builder getClaims() {
return this.claims;
}
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2002-2021 the original author or authors.
* Copyright 2002-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -83,6 +83,12 @@ public class NimbusJwtClientAuthenticationParametersConverterTests {
.withMessage("authorizationGrantRequest cannot be null");
}
@Test
public void setJwtClientAssertionCustomizerWhenNullThenThrowIllegalArgumentException() {
assertThatIllegalArgumentException().isThrownBy(() -> this.converter.setJwtClientAssertionCustomizer(null))
.withMessage("jwtClientAssertionCustomizer cannot be null");
}
@Test
public void convertWhenOtherClientAuthenticationMethodThenNotCustomized() {
// @formatter:off
@ -179,6 +185,51 @@ public class NimbusJwtClientAuthenticationParametersConverterTests {
assertThat(jws.getExpiresAt()).isNotNull();
}
@Test
public void convertWhenJwtClientAssertionCustomizerSetThenUsed() {
OctetSequenceKey secretJwk = TestJwks.DEFAULT_SECRET_JWK;
given(this.jwkResolver.apply(any())).willReturn(secretJwk);
String headerName = "custom-header";
String headerValue = "header-value";
String claimName = "custom-claim";
String claimValue = "claim-value";
this.converter.setJwtClientAssertionCustomizer((context) -> {
context.getHeaders().header(headerName, headerValue);
context.getClaims().claim(claimName, claimValue);
});
// @formatter:off
ClientRegistration clientRegistration = TestClientRegistrations.clientCredentials()
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_JWT)
.build();
// @formatter:on
OAuth2ClientCredentialsGrantRequest clientCredentialsGrantRequest = new OAuth2ClientCredentialsGrantRequest(
clientRegistration);
MultiValueMap<String, String> parameters = this.converter.convert(clientCredentialsGrantRequest);
assertThat(parameters.getFirst(OAuth2ParameterNames.CLIENT_ASSERTION_TYPE))
.isEqualTo("urn:ietf:params:oauth:client-assertion-type:jwt-bearer");
String encodedJws = parameters.getFirst(OAuth2ParameterNames.CLIENT_ASSERTION);
assertThat(encodedJws).isNotNull();
NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withSecretKey(secretJwk.toSecretKey()).build();
Jwt jws = jwtDecoder.decode(encodedJws);
assertThat(jws.getHeaders().get(JoseHeaderNames.ALG)).isEqualTo(MacAlgorithm.HS256.getName());
assertThat(jws.getHeaders().get(JoseHeaderNames.KID)).isEqualTo(secretJwk.getKeyID());
assertThat(jws.getHeaders().get(headerName)).isEqualTo(headerValue);
assertThat(jws.<String>getClaim(JwtClaimNames.ISS)).isEqualTo(clientRegistration.getClientId());
assertThat(jws.getSubject()).isEqualTo(clientRegistration.getClientId());
assertThat(jws.getAudience())
.isEqualTo(Collections.singletonList(clientRegistration.getProviderDetails().getTokenUri()));
assertThat(jws.getId()).isNotNull();
assertThat(jws.getIssuedAt()).isNotNull();
assertThat(jws.getExpiresAt()).isNotNull();
assertThat(jws.getClaimAsString(claimName)).isEqualTo(claimValue);
}
// gh-9814
@Test
public void convertWhenClientKeyChangesThenNewKeyUsed() throws Exception {