Skip to content

Commit 428216b

Browse files
Steve Riesenbergsjohnr
Steve Riesenberg
authored andcommitted
Add support for customizing claims in JWT Client Assertion
Closes gh-9855
1 parent 50a3bcf commit 428216b

File tree

4 files changed

+196
-2
lines changed

4 files changed

+196
-2
lines changed

docs/modules/ROOT/pages/reactive/oauth2/client/client-authentication.adoc

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,3 +149,35 @@ tokenResponseClient.addParametersConverter(
149149
)
150150
----
151151
====
152+
153+
=== Customizing the JWT assertion
154+
155+
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:
156+
157+
====
158+
.Java
159+
[source,java,role="primary"]
160+
----
161+
Function<ClientRegistration, JWK> jwkResolver = ...
162+
163+
NimbusJwtClientAuthenticationParametersConverter<OAuth2ClientCredentialsGrantRequest> converter =
164+
new NimbusJwtClientAuthenticationParametersConverter<>(jwkResolver);
165+
converter.setJwtClientAssertionCustomizer((context) -> {
166+
context.getHeaders().header("custom-header", "header-value");
167+
context.getClaims().claim("custom-claim", "claim-value");
168+
});
169+
----
170+
171+
.Kotlin
172+
[source,kotlin,role="secondary"]
173+
----
174+
val jwkResolver = ...
175+
176+
val converter: NimbusJwtClientAuthenticationParametersConverter<OAuth2ClientCredentialsGrantRequest> =
177+
NimbusJwtClientAuthenticationParametersConverter(jwkResolver)
178+
converter.setJwtClientAssertionCustomizer { context ->
179+
context.headers.header("custom-header", "header-value")
180+
context.claims.claim("custom-claim", "claim-value")
181+
}
182+
----
183+
====

docs/modules/ROOT/pages/servlet/oauth2/client/client-authentication.adoc

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,3 +163,35 @@ val tokenResponseClient = DefaultClientCredentialsTokenResponseClient()
163163
tokenResponseClient.setRequestEntityConverter(requestEntityConverter)
164164
----
165165
====
166+
167+
=== Customizing the JWT assertion
168+
169+
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:
170+
171+
====
172+
.Java
173+
[source,java,role="primary"]
174+
----
175+
Function<ClientRegistration, JWK> jwkResolver = ...
176+
177+
NimbusJwtClientAuthenticationParametersConverter<OAuth2ClientCredentialsGrantRequest> converter =
178+
new NimbusJwtClientAuthenticationParametersConverter<>(jwkResolver);
179+
converter.setJwtClientAssertionCustomizer((context) -> {
180+
context.getHeaders().header("custom-header", "header-value");
181+
context.getClaims().claim("custom-claim", "claim-value");
182+
});
183+
----
184+
185+
.Kotlin
186+
[source,kotlin,role="secondary"]
187+
----
188+
val jwkResolver = ...
189+
190+
val converter: NimbusJwtClientAuthenticationParametersConverter<OAuth2ClientCredentialsGrantRequest> =
191+
NimbusJwtClientAuthenticationParametersConverter(jwkResolver)
192+
converter.setJwtClientAssertionCustomizer { context ->
193+
context.headers.header("custom-header", "header-value")
194+
context.claims.claim("custom-claim", "claim-value")
195+
}
196+
----
197+
====

oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/endpoint/NimbusJwtClientAuthenticationParametersConverter.java

Lines changed: 80 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2021 the original author or authors.
2+
* Copyright 2002-2022 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -22,6 +22,7 @@
2222
import java.util.Map;
2323
import java.util.UUID;
2424
import java.util.concurrent.ConcurrentHashMap;
25+
import java.util.function.Consumer;
2526
import java.util.function.Function;
2627

2728
import com.nimbusds.jose.jwk.JWK;
@@ -62,6 +63,7 @@
6263
*
6364
* @param <T> the type of {@link AbstractOAuth2AuthorizationGrantRequest}
6465
* @author Joe Grandja
66+
* @author Steve Riesenberg
6567
* @since 5.5
6668
* @see Converter
6769
* @see com.nimbusds.jose.jwk.JWK
@@ -87,6 +89,9 @@ public final class NimbusJwtClientAuthenticationParametersConverter<T extends Ab
8789

8890
private final Map<String, JwsEncoderHolder> jwsEncoders = new ConcurrentHashMap<>();
8991

92+
private Consumer<JwtClientAuthenticationContext<T>> jwtClientAssertionCustomizer = (context) -> {
93+
};
94+
9095
/**
9196
* Constructs a {@code NimbusJwtClientAuthenticationParametersConverter} using the
9297
* provided parameters.
@@ -142,6 +147,10 @@ public MultiValueMap<String, String> convert(T authorizationGrantRequest) {
142147
.expiresAt(expiresAt);
143148
// @formatter:on
144149

150+
JwtClientAuthenticationContext<T> jwtClientAssertionContext = new JwtClientAuthenticationContext<>(
151+
authorizationGrantRequest, headersBuilder, claimsBuilder);
152+
this.jwtClientAssertionCustomizer.accept(jwtClientAssertionContext);
153+
145154
JwsHeader jwsHeader = headersBuilder.build();
146155
JwtClaimsSet jwtClaimsSet = claimsBuilder.build();
147156

@@ -189,6 +198,21 @@ else if (KeyType.OCT.equals(jwk.getKeyType())) {
189198
return jwsAlgorithm;
190199
}
191200

201+
/**
202+
* Sets the {@link Consumer} to be provided the
203+
* {@link JwtClientAuthenticationContext}, which contains the
204+
* {@link JwsHeader.Builder} and {@link JwtClaimsSet.Builder} for further
205+
* customization.
206+
* @param jwtClientAssertionCustomizer the {@link Consumer} to be provided the
207+
* {@link JwtClientAuthenticationContext}
208+
* @since 5.7
209+
*/
210+
public void setJwtClientAssertionCustomizer(
211+
Consumer<JwtClientAuthenticationContext<T>> jwtClientAssertionCustomizer) {
212+
Assert.notNull(jwtClientAssertionCustomizer, "jwtClientAssertionCustomizer cannot be null");
213+
this.jwtClientAssertionCustomizer = jwtClientAssertionCustomizer;
214+
}
215+
192216
private static final class JwsEncoderHolder {
193217

194218
private final JwtEncoder jwsEncoder;
@@ -210,4 +234,59 @@ private JWK getJwk() {
210234

211235
}
212236

237+
/**
238+
* A context that holds client authentication-specific state and is used by
239+
* {@link NimbusJwtClientAuthenticationParametersConverter} when attempting to
240+
* customize the JSON Web Token (JWS) client assertion.
241+
*
242+
* @param <T> the type of {@link AbstractOAuth2AuthorizationGrantRequest}
243+
* @since 5.7
244+
*/
245+
public static final class JwtClientAuthenticationContext<T extends AbstractOAuth2AuthorizationGrantRequest> {
246+
247+
private final T authorizationGrantRequest;
248+
249+
private final JwsHeader.Builder headers;
250+
251+
private final JwtClaimsSet.Builder claims;
252+
253+
private JwtClientAuthenticationContext(T authorizationGrantRequest, JwsHeader.Builder headers,
254+
JwtClaimsSet.Builder claims) {
255+
this.authorizationGrantRequest = authorizationGrantRequest;
256+
this.headers = headers;
257+
this.claims = claims;
258+
}
259+
260+
/**
261+
* Returns the {@link AbstractOAuth2AuthorizationGrantRequest authorization grant
262+
* request}.
263+
* @return the {@link AbstractOAuth2AuthorizationGrantRequest authorization grant
264+
* request}
265+
*/
266+
public T getAuthorizationGrantRequest() {
267+
return this.authorizationGrantRequest;
268+
}
269+
270+
/**
271+
* Returns the {@link JwsHeader.Builder} to be used to customize headers of the
272+
* JSON Web Token (JWS).
273+
* @return the {@link JwsHeader.Builder} to be used to customize headers of the
274+
* JSON Web Token (JWS)
275+
*/
276+
public JwsHeader.Builder getHeaders() {
277+
return this.headers;
278+
}
279+
280+
/**
281+
* Returns the {@link JwtClaimsSet.Builder} to be used to customize claims of the
282+
* JSON Web Token (JWS).
283+
* @return the {@link JwtClaimsSet.Builder} to be used to customize claims of the
284+
* JSON Web Token (JWS)
285+
*/
286+
public JwtClaimsSet.Builder getClaims() {
287+
return this.claims;
288+
}
289+
290+
}
291+
213292
}

oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/endpoint/NimbusJwtClientAuthenticationParametersConverterTests.java

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2021 the original author or authors.
2+
* Copyright 2002-2022 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -83,6 +83,12 @@ public void convertWhenAuthorizationGrantRequestNullThenThrowIllegalArgumentExce
8383
.withMessage("authorizationGrantRequest cannot be null");
8484
}
8585

86+
@Test
87+
public void setJwtClientAssertionCustomizerWhenNullThenThrowIllegalArgumentException() {
88+
assertThatIllegalArgumentException().isThrownBy(() -> this.converter.setJwtClientAssertionCustomizer(null))
89+
.withMessage("jwtClientAssertionCustomizer cannot be null");
90+
}
91+
8692
@Test
8793
public void convertWhenOtherClientAuthenticationMethodThenNotCustomized() {
8894
// @formatter:off
@@ -179,6 +185,51 @@ public void convertWhenClientSecretJwtClientAuthenticationMethodThenCustomized()
179185
assertThat(jws.getExpiresAt()).isNotNull();
180186
}
181187

188+
@Test
189+
public void convertWhenJwtClientAssertionCustomizerSetThenUsed() {
190+
OctetSequenceKey secretJwk = TestJwks.DEFAULT_SECRET_JWK;
191+
given(this.jwkResolver.apply(any())).willReturn(secretJwk);
192+
193+
String headerName = "custom-header";
194+
String headerValue = "header-value";
195+
String claimName = "custom-claim";
196+
String claimValue = "claim-value";
197+
this.converter.setJwtClientAssertionCustomizer((context) -> {
198+
context.getHeaders().header(headerName, headerValue);
199+
context.getClaims().claim(claimName, claimValue);
200+
});
201+
202+
// @formatter:off
203+
ClientRegistration clientRegistration = TestClientRegistrations.clientCredentials()
204+
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_JWT)
205+
.build();
206+
// @formatter:on
207+
208+
OAuth2ClientCredentialsGrantRequest clientCredentialsGrantRequest = new OAuth2ClientCredentialsGrantRequest(
209+
clientRegistration);
210+
MultiValueMap<String, String> parameters = this.converter.convert(clientCredentialsGrantRequest);
211+
212+
assertThat(parameters.getFirst(OAuth2ParameterNames.CLIENT_ASSERTION_TYPE))
213+
.isEqualTo("urn:ietf:params:oauth:client-assertion-type:jwt-bearer");
214+
String encodedJws = parameters.getFirst(OAuth2ParameterNames.CLIENT_ASSERTION);
215+
assertThat(encodedJws).isNotNull();
216+
217+
NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withSecretKey(secretJwk.toSecretKey()).build();
218+
Jwt jws = jwtDecoder.decode(encodedJws);
219+
220+
assertThat(jws.getHeaders().get(JoseHeaderNames.ALG)).isEqualTo(MacAlgorithm.HS256.getName());
221+
assertThat(jws.getHeaders().get(JoseHeaderNames.KID)).isEqualTo(secretJwk.getKeyID());
222+
assertThat(jws.getHeaders().get(headerName)).isEqualTo(headerValue);
223+
assertThat(jws.<String>getClaim(JwtClaimNames.ISS)).isEqualTo(clientRegistration.getClientId());
224+
assertThat(jws.getSubject()).isEqualTo(clientRegistration.getClientId());
225+
assertThat(jws.getAudience())
226+
.isEqualTo(Collections.singletonList(clientRegistration.getProviderDetails().getTokenUri()));
227+
assertThat(jws.getId()).isNotNull();
228+
assertThat(jws.getIssuedAt()).isNotNull();
229+
assertThat(jws.getExpiresAt()).isNotNull();
230+
assertThat(jws.getClaimAsString(claimName)).isEqualTo(claimValue);
231+
}
232+
182233
// gh-9814
183234
@Test
184235
public void convertWhenClientKeyChangesThenNewKeyUsed() throws Exception {

0 commit comments

Comments
 (0)