Add support for customizing claims in JWT Client Assertion
Closes gh-9855
This commit is contained in:
parent
50a3bcf728
commit
428216b322
|
@ -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")
|
||||
}
|
||||
----
|
||||
====
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
----
|
||||
====
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
Loading…
Reference in New Issue