Skip to content

Commit

Permalink
Merge pull request #39249 from wesleysalimansdvb/39215-oidc-make-age-…
Browse files Browse the repository at this point in the history
…optional

OIDC make token iat claim optional via config property
  • Loading branch information
sberyozkin authored Mar 8, 2024
2 parents 09ff3b6 + 4d825af commit fb3c0b8
Show file tree
Hide file tree
Showing 5 changed files with 80 additions and 10 deletions.
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."));
}
}
}
}

0 comments on commit fb3c0b8

Please sign in to comment.