Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

OIDC make token iat claim optional via config property #39249

Merged
Merged
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 @@ -1542,6 +1542,16 @@ public static Token fromAudience(String... audience) {
@ConfigItem
public Optional<Duration> age = Optional.empty();

/**
* Require that the token includes a `iat` (issued at) claim
*
* Set this property to `false` if your JWT token does not contain an `iat` (issued at) claim.
* Note that ID token is always required to have an `iat` claim and therefore this property has no impact on the ID
* token verification process.
*/
@ConfigItem(defaultValue = "true")
public boolean issuedAtRequired = true;

/**
* Name of the claim which contains a principal name. By default, the `upn`, `preferred_username` and `sub`
* claims are
Expand Down Expand Up @@ -1769,6 +1779,14 @@ public void setAge(Duration age) {
this.age = Optional.of(age);
}

public boolean isIssuedAtRequired() {
return issuedAtRequired;
}

public void setIssuedAtRequired(boolean issuedAtRequired) {
this.issuedAtRequired = issuedAtRequired;
}

public Optional<String> getDecryptionKeyLocation() {
return decryptionKeyLocation;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -509,7 +509,8 @@ private Uni<TokenVerificationResult> resolveJwksAndVerifyTokenUni(TenantConfigCo
TokenCredential tokenCred,
boolean enforceAudienceVerification, boolean subjectRequired, String nonce) {
return resolvedContext.provider
.getKeyResolverAndVerifyJwtToken(tokenCred, enforceAudienceVerification, subjectRequired, nonce)
.getKeyResolverAndVerifyJwtToken(tokenCred, enforceAudienceVerification, subjectRequired, nonce,
(tokenCred instanceof IdTokenCredential))
.onFailure(f -> fallbackToIntrospectionIfNoMatchingKey(f, resolvedContext))
.recoverWithUni(f -> introspectTokenUni(resolvedContext, tokenCred.getToken(), true));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -158,22 +158,22 @@ private Map<String, String> checkRequiredClaimsProp() {

public TokenVerificationResult verifySelfSignedJwtToken(String token) throws InvalidJwtException {
return verifyJwtTokenInternal(token, true, false, null, SYMMETRIC_ALGORITHM_CONSTRAINTS, new SymmetricKeyResolver(),
true);
true, oidcConfig.token.isIssuedAtRequired());
}

public TokenVerificationResult verifyJwtToken(String token, boolean enforceAudienceVerification, boolean subjectRequired,
String nonce)
throws InvalidJwtException {
return verifyJwtTokenInternal(customizeJwtToken(token), enforceAudienceVerification, subjectRequired, nonce,
(requiredAlgorithmConstraints != null ? requiredAlgorithmConstraints : ASYMMETRIC_ALGORITHM_CONSTRAINTS),
asymmetricKeyResolver, true);
asymmetricKeyResolver, true, oidcConfig.token.isIssuedAtRequired());
}

public TokenVerificationResult verifyLogoutJwtToken(String token) throws InvalidJwtException {
final boolean enforceExpReq = !oidcConfig.token.age.isPresent();
TokenVerificationResult result = verifyJwtTokenInternal(token, true, false, null, ASYMMETRIC_ALGORITHM_CONSTRAINTS,
asymmetricKeyResolver,
enforceExpReq);
enforceExpReq, oidcConfig.token.isIssuedAtRequired());
if (!enforceExpReq) {
// Expiry check was skipped during the initial verification but if the logout token contains the exp claim
// then it must be verified
Expand All @@ -191,7 +191,8 @@ private TokenVerificationResult verifyJwtTokenInternal(String token,
boolean subjectRequired,
String nonce,
AlgorithmConstraints algConstraints,
VerificationKeyResolver verificationKeyResolver, boolean enforceExpReq) throws InvalidJwtException {
VerificationKeyResolver verificationKeyResolver, boolean enforceExpReq, boolean issuedAtRequired)
throws InvalidJwtException {
JwtConsumerBuilder builder = new JwtConsumerBuilder();

builder.setVerificationKeyResolver(verificationKeyResolver);
Expand All @@ -209,7 +210,9 @@ private TokenVerificationResult verifyJwtTokenInternal(String token,
builder.registerValidator(new CustomClaimsValidator(Map.of(OidcConstants.NONCE, nonce)));
}

builder.setRequireIssuedAt();
if (issuedAtRequired) {
builder.setRequireIssuedAt();
}

if (issuer != null) {
builder.setExpectedIssuer(issuer);
Expand Down Expand Up @@ -308,7 +311,7 @@ public Uni<? extends TokenVerificationResult> apply(Void v) {

public Uni<TokenVerificationResult> getKeyResolverAndVerifyJwtToken(TokenCredential tokenCred,
boolean enforceAudienceVerification,
boolean subjectRequired, String nonce) {
boolean subjectRequired, String nonce, boolean issuedAtRequired) {
return keyResolverProvider.resolve(tokenCred).onItem()
.transformToUni(new Function<VerificationKeyResolver, Uni<? extends TokenVerificationResult>>() {

Expand All @@ -321,7 +324,7 @@ public Uni<? extends TokenVerificationResult> apply(VerificationKeyResolver reso
subjectRequired, nonce,
(requiredAlgorithmConstraints != null ? requiredAlgorithmConstraints
: ASYMMETRIC_ALGORITHM_CONSTRAINTS),
resolver, true));
resolver, true, issuedAtRequired));
} catch (Throwable t) {
return Uni.createFrom().failure(t);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -318,6 +318,13 @@ private Uni<TenantConfigContext> createTenantContext(Vertx vertx, OidcTenantConf
}
}

if (!oidcConfig.token.isIssuedAtRequired() && oidcConfig.token.getAge().isPresent()) {
throw new ConfigurationException(
"The 'token.issued-at-required' can only be set to false if 'token.age' is not set." +
" Either set 'token.issued-at-required' to true or do not set 'token.age'.",
Set.of("quarkus.oidc.token.issued-at-required", "quarkus.oidc.token.age"));
}

return createOidcProvider(oidcConfig, tlsConfig, vertx)
.onItem().transform(new Function<OidcProvider, TenantConfigContext>() {
@Override
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
package io.quarkus.oidc.runtime;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;

import java.nio.charset.StandardCharsets;
import java.time.Instant;
import java.util.Base64;

import jakarta.json.Json;
Expand All @@ -15,6 +17,8 @@
import org.jose4j.jwk.EllipticCurveJsonWebKey;
import org.jose4j.jwk.RsaJsonWebKey;
import org.jose4j.jwk.RsaJwkGenerator;
import org.jose4j.jws.AlgorithmIdentifiers;
import org.jose4j.jws.JsonWebSignature;
import org.jose4j.jwt.consumer.InvalidJwtException;
import org.jose4j.keys.EllipticCurves;
import org.jose4j.lang.UnresolvableKeyException;
Expand Down Expand Up @@ -106,7 +110,6 @@ private static String replaceAlgorithm(String token, String algorithm) {

@Test
public void testSubject() throws Exception {

RsaJsonWebKey rsaJsonWebKey = RsaJwkGenerator.generateJwk(2048);
rsaJsonWebKey.setKeyId("k1");
JsonWebKeySet jwkSet = new JsonWebKeySet("{\"keys\": [" + rsaJsonWebKey.toJson() + "]}");
Expand Down Expand Up @@ -134,7 +137,6 @@ public void testSubject() throws Exception {

@Test
public void testNonce() throws Exception {

RsaJsonWebKey rsaJsonWebKey = RsaJwkGenerator.generateJwk(2048);
rsaJsonWebKey.setKeyId("k1");
JsonWebKeySet jwkSet = new JsonWebKeySet("{\"keys\": [" + rsaJsonWebKey.toJson() + "]}");
Expand All @@ -159,4 +161,43 @@ public void testNonce() throws Exception {
}
}
}

@Test
public void testAge() throws Exception {
String tokenPayload = "{\n" +
" \"exp\": " + Instant.now().plusSeconds(1000).getEpochSecond() + "\n" +
"}";

JsonWebSignature jws = new JsonWebSignature();
jws.setPayload(tokenPayload);
jws.setAlgorithmHeaderValue(AlgorithmIdentifiers.RSA_USING_SHA256);

RsaJsonWebKey rsaJsonWebKey = RsaJwkGenerator.generateJwk(2048);

jws.setKey(rsaJsonWebKey.getPrivateKey());

String token = jws.getCompactSerialization();

JsonWebKeySet jwkSet = new JsonWebKeySet("{\"keys\": [" + rsaJsonWebKey.toJson() + "]}");

OidcTenantConfig oidcConfig = new OidcTenantConfig();
oidcConfig.token.issuedAtRequired = false;

try (OidcProvider provider = new OidcProvider(null, oidcConfig, jwkSet, null, null)) {
TokenVerificationResult result = provider.verifyJwtToken(token, false, false, null);
assertNull(result.localVerificationResult.getString(Claims.iat.name()));
}

OidcTenantConfig oidcConfigRequireAge = new OidcTenantConfig();
oidcConfigRequireAge.token.issuedAtRequired = true;

try (OidcProvider provider = new OidcProvider(null, oidcConfigRequireAge, jwkSet, null, null)) {
try {
provider.verifyJwtToken(token, false, false, null);
fail("InvalidJwtException expected");
} catch (InvalidJwtException ex) {
assertTrue(ex.getMessage().contains("No Issued At (iat) claim present."));
}
}
}
}
Loading