Skip to content

Commit 6ec9ca6

Browse files
committed
Allow to keep JWT audience trailing slash
1 parent a044aab commit 6ec9ca6

File tree

9 files changed

+107
-7
lines changed

9 files changed

+107
-7
lines changed

docs/src/main/asciidoc/security-oidc-expanded-configuration.adoc

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -261,6 +261,7 @@ https://openid.net/specs/openid-connect-core-1_0.html#ClientAuthentication[Stand
261261

262262
|quarkus.oidc.credentials.jwt.token-key-id ||The token key id (`kid`) header value
263263
|quarkus.oidc.credentials.jwt.audience |OIDC token endpoint address|Audience (`aud`) claim value
264+
|quarkus.oidc.credentials.jwt.keep-audience-trailing-slash |false|Whether to keep a trailing slash in the audience value
264265
|quarkus.oidc.credentials.jwt.issuer |OIDC client id|Issuer (`iss`) claim value
265266
|quarkus.oidc.credentials.jwt.subject |OIDC client id|Subject (`sub`) cliam value
266267
|quarkus.oidc.credentials.jwt.lifespan |10 seconds|Lifespan added to the `issued at` (`iat`) claim value to calculate the expiry (`exp`) claim value
@@ -270,6 +271,7 @@ https://openid.net/specs/openid-connect-core-1_0.html#ClientAuthentication[Stand
270271
`quarkus.oidc.credentials.jwt.token-key-id` can be used to set a key identifier (`kid`) header value to help the OIDC provider with fidning a token verification key.
271272
All other properties listed in the table above can be used to customize JWT claim values.
272273

274+
273275
==== JWT Bearer
274276

275277
By default, when a client JWT authentication token must be produced, it is generated by Quarkus OIDC. In some cases, the JWT bearer token may be provided and periodically updated by Kubernetes.

extensions/oidc-client/runtime/src/test/java/io/quarkus/oidc/client/OidcClientConfigImpl.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@ enum ConfigMappingMethods {
9191
CREDENTIALS_JWT_LIFESPAN,
9292
CREDENTIALS_JWT_ASSERTION,
9393
CREDENTIALS_JWT_AUDIENCE,
94+
CREDENTIALS_JWT_KEEP_AUDIENCE_TRAILING_SLASH,
9495
CREDENTIALS_JWT_TOKEN_ID,
9596
JWT_BEARER_TOKEN_PATH,
9697
REFRESH_INTERVAL
@@ -265,6 +266,12 @@ public Optional<String> audience() {
265266
return Optional.empty();
266267
}
267268

269+
@Override
270+
public boolean keepAudienceTrailingSlash() {
271+
invocationsRecorder.put(ConfigMappingMethods.CREDENTIALS_JWT_KEEP_AUDIENCE_TRAILING_SLASH, true);
272+
return false;
273+
}
274+
268275
@Override
269276
public Optional<String> tokenKeyId() {
270277
invocationsRecorder.put(ConfigMappingMethods.CREDENTIALS_JWT_TOKEN_ID, true);

extensions/oidc-common/runtime/src/main/java/io/quarkus/oidc/common/runtime/OidcClientCommonConfig.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -330,6 +330,11 @@ public Optional<String> audience() {
330330
return audience;
331331
}
332332

333+
@Override
334+
public boolean keepAudienceTrailingSlash() {
335+
return keepAudienceTrailingSlash;
336+
}
337+
333338
@Override
334339
public Optional<String> tokenKeyId() {
335340
return tokenKeyId;
@@ -434,6 +439,11 @@ public static enum Source {
434439
*/
435440
public Optional<String> audience = Optional.empty();
436441

442+
/**
443+
* Whether to keep a trailing slash `/` in the {@link #audience()} value.
444+
*/
445+
public boolean keepAudienceTrailingSlash = false;
446+
437447
/**
438448
* The key identifier of the signing key added as a JWT `kid` header.
439449
*/
@@ -575,6 +585,7 @@ private void addConfigMappingValues(
575585
keyId = mapping.keyId();
576586
keyPassword = mapping.keyPassword();
577587
audience = mapping.audience();
588+
keepAudienceTrailingSlash = mapping.keepAudienceTrailingSlash();
578589
tokenKeyId = mapping.tokenKeyId();
579590
issuer = mapping.issuer();
580591
subject = mapping.subject();

extensions/oidc-common/runtime/src/main/java/io/quarkus/oidc/common/runtime/OidcCommonUtils.java

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -246,6 +246,10 @@ public static String getAuthServerUrl(OidcCommonConfig oidcConfig) {
246246
return removeLastPathSeparator(oidcConfig.authServerUrl().get());
247247
}
248248

249+
private static String removeAudienceTrailingSlash(Credentials.Jwt jwtConfig, String value) {
250+
return !jwtConfig.keepAudienceTrailingSlash() ? removeLastPathSeparator(value) : value;
251+
}
252+
249253
private static String removeLastPathSeparator(String value) {
250254
return value.endsWith("/") ? value.substring(0, value.length() - 1) : value;
251255
}
@@ -435,9 +439,8 @@ public static String signJwtWithKey(OidcClientCommonConfig oidcConfig, String to
435439
.claims(additionalClaims(oidcConfig.credentials().jwt().claims()))
436440
.issuer(oidcConfig.credentials().jwt().issuer().orElse(oidcConfig.clientId().get()))
437441
.subject(oidcConfig.credentials().jwt().subject().orElse(oidcConfig.clientId().get()))
438-
.audience(oidcConfig.credentials().jwt().audience().isPresent()
439-
? removeLastPathSeparator(oidcConfig.credentials().jwt().audience().get())
440-
: tokenRequestUri)
442+
.audience(removeAudienceTrailingSlash(oidcConfig.credentials().jwt(),
443+
oidcConfig.credentials().jwt().audience().orElse(tokenRequestUri)))
441444
.expiresIn(oidcConfig.credentials().jwt().lifespan()).jws();
442445
if (oidcConfig.credentials().jwt().tokenKeyId().isPresent()) {
443446
jwtSignatureBuilder.keyId(oidcConfig.credentials().jwt().tokenKeyId().get());

extensions/oidc-common/runtime/src/main/java/io/quarkus/oidc/common/runtime/config/OidcClientCommonConfig.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,12 @@ enum Source {
209209
*/
210210
Optional<String> audience();
211211

212+
/**
213+
* Whether to keep a trailing slash `/` in the {@link #audience()} value.
214+
*/
215+
@WithDefault("false")
216+
boolean keepAudienceTrailingSlash();
217+
212218
/**
213219
* The key identifier of the signing key added as a JWT `kid` header.
214220
*/

extensions/oidc-common/runtime/src/main/java/io/quarkus/oidc/common/runtime/config/OidcClientCommonConfigBuilder.java

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -484,8 +484,9 @@ public static final class JwtBuilder<T> {
484484

485485
private record JwtImpl(Source source, Optional<String> secret, Provider secretProvider, Optional<String> key,
486486
Optional<String> keyFile, Optional<String> keyStoreFile, Optional<String> keyStorePassword,
487-
Optional<String> keyId, Optional<String> keyPassword, Optional<String> audience, Optional<String> tokenKeyId,
488-
Optional<String> issuer, Optional<String> subject, Map<String, String> claims,
487+
Optional<String> keyId, Optional<String> keyPassword, Optional<String> audience,
488+
boolean keepAudienceTrailingSlash,
489+
Optional<String> tokenKeyId, Optional<String> issuer, Optional<String> subject, Map<String, String> claims,
489490
Optional<String> signatureAlgorithm, int lifespan, boolean assertion,
490491
Optional<Path> tokenPath) implements Jwt {
491492

@@ -503,6 +504,7 @@ private record JwtImpl(Source source, Optional<String> secret, Provider secretPr
503504
private Optional<String> keyId;
504505
private Optional<String> keyPassword;
505506
private Optional<String> audience;
507+
private boolean keepAudienceTrailingSlash;
506508
private Optional<String> tokenKeyId;
507509
private Optional<String> issuer;
508510
private Optional<String> subject;
@@ -523,6 +525,7 @@ public JwtBuilder() {
523525
this.keyId = Optional.empty();
524526
this.keyPassword = Optional.empty();
525527
this.audience = Optional.empty();
528+
this.keepAudienceTrailingSlash = false;
526529
this.tokenKeyId = Optional.empty();
527530
this.issuer = Optional.empty();
528531
this.subject = Optional.empty();
@@ -548,6 +551,7 @@ private JwtBuilder(CredentialsBuilder<T> builder, Jwt jwt) {
548551
this.keyId = jwt.keyId();
549552
this.keyPassword = jwt.keyPassword();
550553
this.audience = jwt.audience();
554+
this.keepAudienceTrailingSlash = jwt.keepAudienceTrailingSlash();
551555
this.tokenKeyId = jwt.tokenKeyId();
552556
this.issuer = jwt.issuer();
553557
this.subject = jwt.subject();
@@ -666,6 +670,23 @@ public JwtBuilder<T> audience(String audience) {
666670
return this;
667671
}
668672

673+
/**
674+
* @param keepAudienceTrailingSlash {@link Jwt#keepAudienceTrailingSlash()}
675+
* @return this builder
676+
*/
677+
public JwtBuilder<T> keepAudienceTrailingSlash() {
678+
return keepAudienceTrailingSlash(true);
679+
}
680+
681+
/**
682+
* @param keepAudienceTrailingSlash {@link Jwt#keepAudienceTrailingSlash()}
683+
* @return this builder
684+
*/
685+
public JwtBuilder<T> keepAudienceTrailingSlash(boolean keepAudienceTrailingSlash) {
686+
this.keepAudienceTrailingSlash = keepAudienceTrailingSlash;
687+
return this;
688+
}
689+
669690
/**
670691
* @param tokenKeyId {@link Jwt#tokenKeyId()}
671692
* @return this builder
@@ -768,7 +789,8 @@ public T endCredentials() {
768789
*/
769790
public Jwt build() {
770791
return new JwtImpl(source, secret, secretProvider, key, keyFile, keyStoreFile, keyStorePassword, keyId, keyPassword,
771-
audience, tokenKeyId, issuer, subject, Map.copyOf(claims), signatureAlgorithm, lifespan, assertion,
792+
audience, keepAudienceTrailingSlash, tokenKeyId, issuer, subject, Map.copyOf(claims), signatureAlgorithm,
793+
lifespan, assertion,
772794
tokenPath);
773795
}
774796
}

extensions/oidc-common/runtime/src/test/java/io/quarkus/oidc/common/runtime/OidcClientCommonConfigBuilderTest.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ public void testCredentialsBuilderDefaultValues() {
4242
assertTrue(jwt.keyId().isEmpty());
4343
assertTrue(jwt.keyPassword().isEmpty());
4444
assertTrue(jwt.audience().isEmpty());
45+
assertFalse(jwt.keepAudienceTrailingSlash());
4546
assertTrue(jwt.tokenKeyId().isEmpty());
4647
assertTrue(jwt.issuer().isEmpty());
4748
assertTrue(jwt.subject().isEmpty());

extensions/oidc-common/runtime/src/test/java/io/quarkus/oidc/common/runtime/OidcCommonUtilsTest.java

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,10 +56,51 @@ public void testJwtTokenWithScope() throws Exception {
5656
cfg.setClientId("client");
5757
cfg.credentials.jwt.claims.put("scope", "read,write");
5858
PrivateKey key = KeyPairGenerator.getInstance("RSA").generateKeyPair().getPrivate();
59-
String jwt = OidcCommonUtils.signJwtWithKey(cfg, "http://localhost", key);
59+
String jwt = OidcCommonUtils.signJwtWithKey(cfg, "http://some.service.com", key);
6060
JsonObject json = decodeJwtContent(jwt);
6161
String scope = json.getString("scope");
6262
assertEquals("read,write", scope);
63+
assertEquals("http://some.service.com", json.getString("aud"));
64+
}
65+
66+
@Test
67+
public void testSignWithAudience() throws Exception {
68+
OidcClientCommonConfig cfg = new OidcClientCommonConfig() {
69+
};
70+
cfg.setClientId("client");
71+
cfg.credentials.jwt.audience = Optional.of("https://server.example.com");
72+
73+
PrivateKey key = KeyPairGenerator.getInstance("RSA").generateKeyPair().getPrivate();
74+
String jwt = OidcCommonUtils.signJwtWithKey(cfg, "http://localhost", key);
75+
JsonObject json = decodeJwtContent(jwt);
76+
assertEquals("https://server.example.com", json.getString("aud"));
77+
}
78+
79+
@Test
80+
public void testSignWithAudienceRemoveTrailingSlash() throws Exception {
81+
OidcClientCommonConfig cfg = new OidcClientCommonConfig() {
82+
};
83+
cfg.setClientId("client");
84+
cfg.credentials.jwt.audience = Optional.of("https://server.example.com/");
85+
86+
PrivateKey key = KeyPairGenerator.getInstance("RSA").generateKeyPair().getPrivate();
87+
String jwt = OidcCommonUtils.signJwtWithKey(cfg, "http://localhost", key);
88+
JsonObject json = decodeJwtContent(jwt);
89+
assertEquals("https://server.example.com", json.getString("aud"));
90+
}
91+
92+
@Test
93+
public void testSignWithAudienceKeepTrailingSlash() throws Exception {
94+
OidcClientCommonConfig cfg = new OidcClientCommonConfig() {
95+
};
96+
cfg.setClientId("client");
97+
cfg.credentials.jwt.audience = Optional.of("https://server.example.com/");
98+
cfg.credentials.jwt.keepAudienceTrailingSlash = true;
99+
100+
PrivateKey key = KeyPairGenerator.getInstance("RSA").generateKeyPair().getPrivate();
101+
String jwt = OidcCommonUtils.signJwtWithKey(cfg, "http://localhost", key);
102+
JsonObject json = decodeJwtContent(jwt);
103+
assertEquals("https://server.example.com/", json.getString("aud"));
63104
}
64105

65106
public static JsonObject decodeJwtContent(String jwt) {

extensions/oidc/runtime/src/test/java/io/quarkus/oidc/runtime/OidcTenantConfigImpl.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ enum ConfigMappingMethods {
8686
CREDENTIALS_JWT_LIFESPAN,
8787
CREDENTIALS_JWT_ASSERTION,
8888
CREDENTIALS_JWT_AUDIENCE,
89+
CREDENTIALS_JWT_KEEP_AUDIENCE_TRAILING_SLASH,
8990
CREDENTIALS_JWT_TOKEN_ID,
9091
PROVIDER,
9192
JWKS,
@@ -1120,6 +1121,12 @@ public Optional<String> audience() {
11201121
invocationsRecorder.put(ConfigMappingMethods.CREDENTIALS_JWT_AUDIENCE, true);
11211122
return Optional.empty();
11221123
}
1124+
1125+
@Override
1126+
public boolean keepAudienceTrailingSlash() {
1127+
invocationsRecorder.put(ConfigMappingMethods.CREDENTIALS_JWT_KEEP_AUDIENCE_TRAILING_SLASH, true);
1128+
return false;
1129+
}
11231130

11241131
@Override
11251132
public Optional<String> tokenKeyId() {

0 commit comments

Comments
 (0)