Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,7 @@ https://openid.net/specs/openid-connect-core-1_0.html#ClientAuthentication[Stand

|quarkus.oidc.credentials.jwt.token-key-id ||The token key id (`kid`) header value
|quarkus.oidc.credentials.jwt.audience |OIDC token endpoint address|Audience (`aud`) claim value
|quarkus.oidc.credentials.jwt.keep-audience-trailing-slash |false|Whether to keep a trailing slash in the audience value
|quarkus.oidc.credentials.jwt.issuer |OIDC client id|Issuer (`iss`) claim value
|quarkus.oidc.credentials.jwt.subject |OIDC client id|Subject (`sub`) cliam value
|quarkus.oidc.credentials.jwt.lifespan |10 seconds|Lifespan added to the `issued at` (`iat`) claim value to calculate the expiry (`exp`) claim value
Expand All @@ -270,6 +271,7 @@ https://openid.net/specs/openid-connect-core-1_0.html#ClientAuthentication[Stand
`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.
All other properties listed in the table above can be used to customize JWT claim values.


==== JWT Bearer

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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ enum ConfigMappingMethods {
CREDENTIALS_JWT_LIFESPAN,
CREDENTIALS_JWT_ASSERTION,
CREDENTIALS_JWT_AUDIENCE,
CREDENTIALS_JWT_KEEP_AUDIENCE_TRAILING_SLASH,
CREDENTIALS_JWT_TOKEN_ID,
JWT_BEARER_TOKEN_PATH,
REFRESH_INTERVAL
Expand Down Expand Up @@ -265,6 +266,12 @@ public Optional<String> audience() {
return Optional.empty();
}

@Override
public boolean keepAudienceTrailingSlash() {
invocationsRecorder.put(ConfigMappingMethods.CREDENTIALS_JWT_KEEP_AUDIENCE_TRAILING_SLASH, true);
return false;
}

@Override
public Optional<String> tokenKeyId() {
invocationsRecorder.put(ConfigMappingMethods.CREDENTIALS_JWT_TOKEN_ID, true);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -330,6 +330,11 @@ public Optional<String> audience() {
return audience;
}

@Override
public boolean keepAudienceTrailingSlash() {
return keepAudienceTrailingSlash;
}

@Override
public Optional<String> tokenKeyId() {
return tokenKeyId;
Expand Down Expand Up @@ -434,6 +439,11 @@ public static enum Source {
*/
public Optional<String> audience = Optional.empty();

/**
* Whether to keep a trailing slash `/` in the {@link #audience()} value.
*/
public boolean keepAudienceTrailingSlash = false;

/**
* The key identifier of the signing key added as a JWT `kid` header.
*/
Expand Down Expand Up @@ -575,6 +585,7 @@ private void addConfigMappingValues(
keyId = mapping.keyId();
keyPassword = mapping.keyPassword();
audience = mapping.audience();
keepAudienceTrailingSlash = mapping.keepAudienceTrailingSlash();
tokenKeyId = mapping.tokenKeyId();
issuer = mapping.issuer();
subject = mapping.subject();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,10 @@ public static String getAuthServerUrl(OidcCommonConfig oidcConfig) {
return removeLastPathSeparator(oidcConfig.authServerUrl().get());
}

private static String removeAudienceTrailingSlash(Credentials.Jwt jwtConfig, String value) {
return !jwtConfig.keepAudienceTrailingSlash() ? removeLastPathSeparator(value) : value;
}

private static String removeLastPathSeparator(String value) {
return value.endsWith("/") ? value.substring(0, value.length() - 1) : value;
}
Expand Down Expand Up @@ -435,9 +439,8 @@ public static String signJwtWithKey(OidcClientCommonConfig oidcConfig, String to
.claims(additionalClaims(oidcConfig.credentials().jwt().claims()))
.issuer(oidcConfig.credentials().jwt().issuer().orElse(oidcConfig.clientId().get()))
.subject(oidcConfig.credentials().jwt().subject().orElse(oidcConfig.clientId().get()))
.audience(oidcConfig.credentials().jwt().audience().isPresent()
? removeLastPathSeparator(oidcConfig.credentials().jwt().audience().get())
: tokenRequestUri)
.audience(removeAudienceTrailingSlash(oidcConfig.credentials().jwt(),
oidcConfig.credentials().jwt().audience().orElse(tokenRequestUri)))
.expiresIn(oidcConfig.credentials().jwt().lifespan()).jws();
if (oidcConfig.credentials().jwt().tokenKeyId().isPresent()) {
jwtSignatureBuilder.keyId(oidcConfig.credentials().jwt().tokenKeyId().get());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,12 @@ enum Source {
*/
Optional<String> audience();

/**
* Whether to keep a trailing slash `/` in the {@link #audience()} value.
*/
@WithDefault("false")
boolean keepAudienceTrailingSlash();

/**
* The key identifier of the signing key added as a JWT `kid` header.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -484,8 +484,9 @@ public static final class JwtBuilder<T> {

private record JwtImpl(Source source, Optional<String> secret, Provider secretProvider, Optional<String> key,
Optional<String> keyFile, Optional<String> keyStoreFile, Optional<String> keyStorePassword,
Optional<String> keyId, Optional<String> keyPassword, Optional<String> audience, Optional<String> tokenKeyId,
Optional<String> issuer, Optional<String> subject, Map<String, String> claims,
Optional<String> keyId, Optional<String> keyPassword, Optional<String> audience,
boolean keepAudienceTrailingSlash,
Optional<String> tokenKeyId, Optional<String> issuer, Optional<String> subject, Map<String, String> claims,
Optional<String> signatureAlgorithm, int lifespan, boolean assertion,
Optional<Path> tokenPath) implements Jwt {

Expand All @@ -503,6 +504,7 @@ private record JwtImpl(Source source, Optional<String> secret, Provider secretPr
private Optional<String> keyId;
private Optional<String> keyPassword;
private Optional<String> audience;
private boolean keepAudienceTrailingSlash;
private Optional<String> tokenKeyId;
private Optional<String> issuer;
private Optional<String> subject;
Expand All @@ -523,6 +525,7 @@ public JwtBuilder() {
this.keyId = Optional.empty();
this.keyPassword = Optional.empty();
this.audience = Optional.empty();
this.keepAudienceTrailingSlash = false;
this.tokenKeyId = Optional.empty();
this.issuer = Optional.empty();
this.subject = Optional.empty();
Expand All @@ -548,6 +551,7 @@ private JwtBuilder(CredentialsBuilder<T> builder, Jwt jwt) {
this.keyId = jwt.keyId();
this.keyPassword = jwt.keyPassword();
this.audience = jwt.audience();
this.keepAudienceTrailingSlash = jwt.keepAudienceTrailingSlash();
this.tokenKeyId = jwt.tokenKeyId();
this.issuer = jwt.issuer();
this.subject = jwt.subject();
Expand Down Expand Up @@ -666,6 +670,23 @@ public JwtBuilder<T> audience(String audience) {
return this;
}

/**
* @param keepAudienceTrailingSlash {@link Jwt#keepAudienceTrailingSlash()}
* @return this builder
*/
public JwtBuilder<T> keepAudienceTrailingSlash() {
return keepAudienceTrailingSlash(true);
}

/**
* @param keepAudienceTrailingSlash {@link Jwt#keepAudienceTrailingSlash()}
* @return this builder
*/
public JwtBuilder<T> keepAudienceTrailingSlash(boolean keepAudienceTrailingSlash) {
this.keepAudienceTrailingSlash = keepAudienceTrailingSlash;
return this;
}

/**
* @param tokenKeyId {@link Jwt#tokenKeyId()}
* @return this builder
Expand Down Expand Up @@ -768,7 +789,8 @@ public T endCredentials() {
*/
public Jwt build() {
return new JwtImpl(source, secret, secretProvider, key, keyFile, keyStoreFile, keyStorePassword, keyId, keyPassword,
audience, tokenKeyId, issuer, subject, Map.copyOf(claims), signatureAlgorithm, lifespan, assertion,
audience, keepAudienceTrailingSlash, tokenKeyId, issuer, subject, Map.copyOf(claims), signatureAlgorithm,
lifespan, assertion,
tokenPath);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ public void testCredentialsBuilderDefaultValues() {
assertTrue(jwt.keyId().isEmpty());
assertTrue(jwt.keyPassword().isEmpty());
assertTrue(jwt.audience().isEmpty());
assertFalse(jwt.keepAudienceTrailingSlash());
assertTrue(jwt.tokenKeyId().isEmpty());
assertTrue(jwt.issuer().isEmpty());
assertTrue(jwt.subject().isEmpty());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,10 +56,51 @@ public void testJwtTokenWithScope() throws Exception {
cfg.setClientId("client");
cfg.credentials.jwt.claims.put("scope", "read,write");
PrivateKey key = KeyPairGenerator.getInstance("RSA").generateKeyPair().getPrivate();
String jwt = OidcCommonUtils.signJwtWithKey(cfg, "http://localhost", key);
String jwt = OidcCommonUtils.signJwtWithKey(cfg, "http://some.service.com", key);
JsonObject json = decodeJwtContent(jwt);
String scope = json.getString("scope");
assertEquals("read,write", scope);
assertEquals("http://some.service.com", json.getString("aud"));
}

@Test
public void testSignWithAudience() throws Exception {
OidcClientCommonConfig cfg = new OidcClientCommonConfig() {
};
cfg.setClientId("client");
cfg.credentials.jwt.audience = Optional.of("https://server.example.com");

PrivateKey key = KeyPairGenerator.getInstance("RSA").generateKeyPair().getPrivate();
String jwt = OidcCommonUtils.signJwtWithKey(cfg, "http://localhost", key);
JsonObject json = decodeJwtContent(jwt);
assertEquals("https://server.example.com", json.getString("aud"));
}

@Test
public void testSignWithAudienceRemoveTrailingSlash() throws Exception {
OidcClientCommonConfig cfg = new OidcClientCommonConfig() {
};
cfg.setClientId("client");
cfg.credentials.jwt.audience = Optional.of("https://server.example.com/");

PrivateKey key = KeyPairGenerator.getInstance("RSA").generateKeyPair().getPrivate();
String jwt = OidcCommonUtils.signJwtWithKey(cfg, "http://localhost", key);
JsonObject json = decodeJwtContent(jwt);
assertEquals("https://server.example.com", json.getString("aud"));
}

@Test
public void testSignWithAudienceKeepTrailingSlash() throws Exception {
OidcClientCommonConfig cfg = new OidcClientCommonConfig() {
};
cfg.setClientId("client");
cfg.credentials.jwt.audience = Optional.of("https://server.example.com/");
cfg.credentials.jwt.keepAudienceTrailingSlash = true;

PrivateKey key = KeyPairGenerator.getInstance("RSA").generateKeyPair().getPrivate();
String jwt = OidcCommonUtils.signJwtWithKey(cfg, "http://localhost", key);
JsonObject json = decodeJwtContent(jwt);
assertEquals("https://server.example.com/", json.getString("aud"));
}

public static JsonObject decodeJwtContent(String jwt) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ enum ConfigMappingMethods {
CREDENTIALS_JWT_LIFESPAN,
CREDENTIALS_JWT_ASSERTION,
CREDENTIALS_JWT_AUDIENCE,
CREDENTIALS_JWT_KEEP_AUDIENCE_TRAILING_SLASH,
CREDENTIALS_JWT_TOKEN_ID,
PROVIDER,
JWKS,
Expand Down Expand Up @@ -1121,6 +1122,12 @@ public Optional<String> audience() {
return Optional.empty();
}

@Override
public boolean keepAudienceTrailingSlash() {
invocationsRecorder.put(ConfigMappingMethods.CREDENTIALS_JWT_KEEP_AUDIENCE_TRAILING_SLASH, true);
return false;
}

@Override
public Optional<String> tokenKeyId() {
invocationsRecorder.put(ConfigMappingMethods.CREDENTIALS_JWT_TOKEN_ID, true);
Expand Down
Loading