[java-idp-oidc] branch main updated: JOIDC-152 - Implement maximum refresh time and/or maximum refresh uses.
Henri Mikkonen
henri.mikkonen at iki.fi
Fri Apr 28 15:31:32 UTC 2023
This is an automated email from the git hooks/post-receive script.
hjmikkon pushed a commit to branch main
in repository java-idp-oidc.
View the commit online:
http://git.shibboleth.net/view/?p=java-idp-oidc.git;a=commit;h=4c65db2b36d36c7c84b82386f297a9569fbeb9f9
The following commit(s) were added to refs/heads/main by this push:
new 4c65db2b JOIDC-152 - Implement maximum refresh time and/or maximum refresh uses.
4c65db2b is described below
commit 4c65db2b36d36c7c84b82386f297a9569fbeb9f9
Author: Henri Mikkonen <henri.mikkonen at iki.fi>
AuthorDate: Fri Apr 28 18:31:17 2023 +0300
JOIDC-152 - Implement maximum refresh time and/or maximum refresh uses.
https://shibboleth.atlassian.net/browse/JOIDC-152
Removed refreshTokenLifetime and brought refreshTokenTimeout and
refreshTokenChainLifetime with default properties
Exploit them in grant validation and in refresh token issuance.
Also in introspection and revocation flows.
The chain expiration claim will be stored inside the refresh token
claims sets from now on.
---
.../op/token/support/RefreshTokenClaimsSet.java | 59 +++++++++++++
.../impl/SetRefreshTokenToResponseContext.java | 94 ++++++++++++++++----
.../plugin/oidc/op/profile/impl/ValidateGrant.java | 44 +++++++++-
.../claims/impl/ChainExpiryClaimsValidator.java | 99 ++++++++++++++++++++++
.../idp/service/relying-party/postconfig.xml | 26 ++++--
.../op/profile/flow/AbstractOidcApiFlowTest.java | 43 ++++++----
.../op/profile/flow/IntrospectionFlowTest.java | 41 +++++++++
.../oidc/op/profile/flow/RevocationFlowTest.java | 15 ++++
.../plugin/oidc/op/profile/flow/TokenFlowTest.java | 92 ++++++++++++++++----
.../impl/SetRefreshTokenToResponseContextTest.java | 39 +++++++++
.../oidc/op/profile/impl/ValidateGrantTest.java | 23 ++++-
11 files changed, 520 insertions(+), 55 deletions(-)
diff --git a/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/token/support/RefreshTokenClaimsSet.java b/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/token/support/RefreshTokenClaimsSet.java
index fe151743..6445701f 100644
--- a/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/token/support/RefreshTokenClaimsSet.java
+++ b/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/token/support/RefreshTokenClaimsSet.java
@@ -18,20 +18,26 @@
package net.shibboleth.idp.plugin.oidc.op.token.support;
import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
import com.nimbusds.jwt.JWTClaimsSet;
import com.nimbusds.openid.connect.sdk.claims.ACR;
import net.shibboleth.utilities.java.support.annotation.constraint.NotEmpty;
+import net.shibboleth.utilities.java.support.logic.Constraint;
import net.shibboleth.utilities.java.support.security.DataSealer;
import net.shibboleth.utilities.java.support.security.DataSealerException;
import java.text.ParseException;
import java.time.Instant;
+import java.util.Date;
/** Class wrapping claims set for refresh token. */
public final class RefreshTokenClaimsSet extends TokenClaimsSet {
+ /** Expiration time of the refresh token chain. */
+ @Nonnull @NotEmpty public static final String KEY_CHAIN_EXPIRATION_TIME = "c_exp";
+
/** Value of refresh token claims set type. */
@Nonnull @NotEmpty private static final String VALUE_TYPE_RF = "rf";
@@ -60,6 +66,22 @@ public final class RefreshTokenClaimsSet extends TokenClaimsSet {
super(refreshTokenClaimsSet);
}
+ /**
+ * Get expiration time of the token.
+ *
+ * @return expiration time of the token
+ */
+ @Nonnull public Instant getChainExp() {
+ Constraint.isNotNull(getClaimsSet(), "JWTClaimsSet cannot be null");
+ try {
+ if (getClaimsSet().getClaims().containsKey(KEY_CHAIN_EXPIRATION_TIME)) {
+ return getClaimsSet().getDateClaim(KEY_CHAIN_EXPIRATION_TIME).toInstant();
+ }
+ } catch (final ParseException e) {
+ }
+ return null;
+ }
+
/**
* Parses refresh token from string (JSON).
*
@@ -72,6 +94,10 @@ public final class RefreshTokenClaimsSet extends TokenClaimsSet {
final JWTClaimsSet atClaimsSet = JWTClaimsSet.parse(refreshTokenClaimsSet);
// Throws exception if parsing result is not expected one.
verifyParsedClaims(VALUE_TYPE_RF, atClaimsSet);
+ if (atClaimsSet.getClaims().containsKey(KEY_CHAIN_EXPIRATION_TIME)) {
+ atClaimsSet.getDateClaim(KEY_CHAIN_EXPIRATION_TIME);
+ }
+
return new RefreshTokenClaimsSet(atClaimsSet);
}
@@ -121,6 +147,25 @@ public final class RefreshTokenClaimsSet extends TokenClaimsSet {
setIssuedAt(iat);
setExpiresAt(exp);
}
+
+ /**
+ * Constructor for refresh token claims set when derived from existing claims set.
+ *
+ * @param existing Authorize Code / Refresh Token this token is based on
+ * @param iat Issue time of the token
+ * @param exp Expiration time of the token
+ * @param chainExp Expiration time of the chain
+ *
+ * @since 3.4.0
+ */
+ public Builder(@Nonnull final TokenClaimsSet existing, @Nonnull final Instant iat, @Nonnull final Instant exp,
+ @Nonnull final Instant chainExp) {
+ this(existing);
+ setIssuedAt(iat);
+ setExpiresAt(exp);
+ setChainExpiresAt(chainExp);
+ }
+
// Checkstyle: ParameterNumber ON
/**
@@ -148,6 +193,20 @@ public final class RefreshTokenClaimsSet extends TokenClaimsSet {
setConsentEnabled(existing.isConsentEnabled());
setSessionIdentifier(existing.getSessionIdentifier());
}
+
+ /**
+ * Set the chain expiration time.
+ *
+ * @param i time
+ *
+ * @return the builder
+ *
+ * @since 3.4.0
+ */
+ public Builder setChainExpiresAt(@Nonnull final Instant i) {
+ addCustomClaim(KEY_CHAIN_EXPIRATION_TIME, Date.from(i));
+ return this;
+ }
/**
* Builds RefreshTokenClaimsSet.
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/SetRefreshTokenToResponseContext.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/SetRefreshTokenToResponseContext.java
index 3478724f..d66cee20 100644
--- a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/SetRefreshTokenToResponseContext.java
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/SetRefreshTokenToResponseContext.java
@@ -44,8 +44,9 @@ import net.shibboleth.idp.plugin.oidc.op.token.support.RefreshTokenClaimsSet;
import net.shibboleth.idp.plugin.oidc.op.token.support.TokenClaimsSet;
import net.shibboleth.idp.profile.IdPEventIds;
import net.shibboleth.oidc.profile.config.logic.EnforceRefreshTokenRotationPredicate;
+import net.shibboleth.oidc.profile.config.navigate.RefreshTokenChainLifetimeLookupFunction;
import net.shibboleth.oidc.profile.config.navigate.RefreshTokenClaimsSetManipulationStrategyLookupFunction;
-import net.shibboleth.oidc.profile.config.navigate.RefreshTokenLifetimeLookupFunction;
+import net.shibboleth.oidc.profile.config.navigate.RefreshTokenTimeoutLookupFunction;
import org.opensaml.profile.action.ActionSupport;
import net.shibboleth.utilities.java.support.annotation.ParameterName;
@@ -77,8 +78,11 @@ public class SetRefreshTokenToResponseContext extends AbstractOIDCResponseAction
/** Message revocation cache instance to use. */
@NonnullAfterInit private RevocationCache revocationCache;
- /** Strategy used to obtain the refresh token lifetime. */
- @Nonnull private Function<ProfileRequestContext,Duration> refreshTokenLifetimeLookupStrategy;
+ /** Strategy used to obtain the refresh token chain lifetime. */
+ @Nonnull private Function<ProfileRequestContext,Duration> refreshTokenChainLifetimeLookupStrategy;
+
+ /** Strategy used to obtain the refresh token timeout. */
+ @Nonnull private Function<ProfileRequestContext,Duration> refreshTokenTimeoutLookupStrategy;
/** Lookup function to supply strategy bi-function for manipulating token claims set. */
@Nonnull
@@ -100,8 +104,11 @@ public class SetRefreshTokenToResponseContext extends AbstractOIDCResponseAction
/** Authorize Code / Refresh Token the refresh token will be based on. */
@Nullable private TokenClaimsSet tokenClaimsSet;
- /** Refresh Token lifetime. */
- @Nullable private Duration refreshTokenLifetime;
+ /** Refresh Token chain lifetime. */
+ @Nullable private Duration refreshTokenChainLifetime;
+
+ /** Refresh Token timeout. */
+ @Nullable private Duration refreshTokenTimeout;
/** The generator to use. */
@Nullable private IdentifierGenerationStrategy idGenerator;
@@ -112,7 +119,8 @@ public class SetRefreshTokenToResponseContext extends AbstractOIDCResponseAction
* @param sealer sealer to encrypt/hmac refresh token.
*/
public SetRefreshTokenToResponseContext(@Nonnull @ParameterName(name = "sealer") final DataSealer sealer) {
- refreshTokenLifetimeLookupStrategy = new RefreshTokenLifetimeLookupFunction();
+ refreshTokenChainLifetimeLookupStrategy = new RefreshTokenChainLifetimeLookupFunction();
+ refreshTokenTimeoutLookupStrategy = new RefreshTokenTimeoutLookupFunction();
dataSealer = Constraint.isNotNull(sealer, "DataSealer cannot be null");
tokenClaimsSetManipulationStrategyLookupStrategy =
new RefreshTokenClaimsSetManipulationStrategyLookupFunction();
@@ -132,16 +140,29 @@ public class SetRefreshTokenToResponseContext extends AbstractOIDCResponseAction
}
/**
- * Set the strategy used to obtain the access token lifetime.
+ * Set the strategy used to obtain the refresh token chain lifetime.
*
* @param strategy lookup strategy
*/
- public void setRefreshTokenLifetimeLookupStrategy(
+ public void setRefreshTokenChainLifetimeLookupStrategy(
@Nonnull final Function<ProfileRequestContext,Duration> strategy) {
ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
- refreshTokenLifetimeLookupStrategy =
- Constraint.isNotNull(strategy, "Refresh token lifetime lookup strategy cannot be null");
+ refreshTokenChainLifetimeLookupStrategy =
+ Constraint.isNotNull(strategy, "Refresh token chain lifetime lookup strategy cannot be null");
+ }
+
+ /**
+ * Set the strategy used to obtain the refresh token timeout.
+ *
+ * @param strategy lookup strategy
+ */
+ public void setRefreshTokenTimeoutLookupStrategy(
+ @Nonnull final Function<ProfileRequestContext,Duration> strategy) {
+ ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+
+ refreshTokenTimeoutLookupStrategy =
+ Constraint.isNotNull(strategy, "Refresh token timeout lookup strategy cannot be null");
}
/**
@@ -209,13 +230,20 @@ public class SetRefreshTokenToResponseContext extends AbstractOIDCResponseAction
return false;
}
- refreshTokenLifetime = refreshTokenLifetimeLookupStrategy.apply(profileRequestContext);
- if (refreshTokenLifetime == null) {
+ refreshTokenChainLifetime = refreshTokenChainLifetimeLookupStrategy.apply(profileRequestContext);
+ if (refreshTokenChainLifetime == null) {
log.warn("{} No lifetime supplied for refresh token", getLogPrefix());
ActionSupport.buildEvent(profileRequestContext, IdPEventIds.INVALID_PROFILE_CONFIG);
return false;
}
-
+
+ refreshTokenTimeout = refreshTokenTimeoutLookupStrategy.apply(profileRequestContext);
+ if (refreshTokenTimeout == null) {
+ log.warn("{} No timeout supplied for refresh token", getLogPrefix());
+ ActionSupport.buildEvent(profileRequestContext, IdPEventIds.INVALID_PROFILE_CONFIG);
+ return false;
+ }
+
tokenClaimsSet = getOidcResponseContext().getAuthorizationGrantClaimsSet();
if (tokenClaimsSet == null || !(tokenClaimsSet instanceof RefreshTokenClaimsSet)
&& !(tokenClaimsSet instanceof AuthorizeCodeClaimsSet)) {
@@ -241,11 +269,15 @@ public class SetRefreshTokenToResponseContext extends AbstractOIDCResponseAction
/** {@inheritDoc} */
@Override
protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
- final Instant dateExp = Instant.now().plus(refreshTokenLifetime);
+
+ final Instant chainExp = calculateChainExp();
+ final Instant tokenExp = Instant.now().plus(refreshTokenTimeout);
+
final String rootTokenId = StringSupport.trimOrNull(tokenClaimsSet.getRootTokenIdentifier()) == null ?
tokenClaimsSet.getID() : tokenClaimsSet.getRootTokenIdentifier();
final RefreshTokenClaimsSet claimsSet =
- new RefreshTokenClaimsSet.Builder(tokenClaimsSet, Instant.now(), dateExp)
+ new RefreshTokenClaimsSet.Builder(tokenClaimsSet, Instant.now(),
+ chainExp.isBefore(tokenExp) ? chainExp : tokenExp, chainExp)
.setJWTID(idGenerator)
.setRootTokenIdentifier(rootTokenId)
.build();
@@ -301,4 +333,36 @@ public class SetRefreshTokenToResponseContext extends AbstractOIDCResponseAction
// Checkstyle: CyclomaticComplexity ON
+ /**
+ * Get the possibly existing chain expiration instant from the claims set.
+ *
+ * @param claimsSet the claims set input
+ * @return the existing value if exists, null otherwise
+ */
+ protected Instant getExistingChainExp(final JWTClaimsSet claimsSet) {
+ if (claimsSet.getClaims().containsKey(RefreshTokenClaimsSet.KEY_CHAIN_EXPIRATION_TIME)) {
+ try {
+ return tokenClaimsSet.getClaimsSet()
+ .getDateClaim(RefreshTokenClaimsSet.KEY_CHAIN_EXPIRATION_TIME).toInstant();
+ } catch (ParseException e) {
+ log.warn("{} Could not parse the chain expiration time from the claims set", e);
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Calculates the chain expiration time by taking the closest from the existing item in the claims set (if exists)
+ * and the value calculated via current profile configuration.
+ *
+ * @return the instant to be used as the chain expiration time
+ */
+ protected Instant calculateChainExp() {
+ final Instant chainExp = tokenClaimsSet.getAuthenticationTime().plus(refreshTokenChainLifetime);
+ final Instant existingChainExp = getExistingChainExp(tokenClaimsSet.getClaimsSet());
+ if (existingChainExp == null) {
+ return chainExp;
+ }
+ return chainExp.isBefore(existingChainExp) ? chainExp : existingChainExp;
+ }
}
\ No newline at end of file
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/ValidateGrant.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/ValidateGrant.java
index 9f31308d..dcf01fc3 100644
--- a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/ValidateGrant.java
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/ValidateGrant.java
@@ -19,6 +19,7 @@ package net.shibboleth.idp.plugin.oidc.op.profile.impl;
import java.text.ParseException;
import java.time.Duration;
+import java.time.Instant;
import java.util.function.Function;
import java.util.function.Predicate;
@@ -47,6 +48,7 @@ import net.shibboleth.idp.plugin.oidc.op.token.support.TokenClaimsSet;
import net.shibboleth.idp.profile.IdPEventIds;
import net.shibboleth.idp.profile.context.RelyingPartyContext;
import net.shibboleth.oidc.profile.config.logic.RefreshTokensEnabledPredicate;
+import net.shibboleth.oidc.profile.config.navigate.RefreshTokenChainLifetimeLookupFunction;
import net.shibboleth.oidc.profile.config.navigate.RevocationLifetimeLookupFunction;
import net.shibboleth.oidc.profile.core.OidcEventIds;
import net.shibboleth.utilities.java.support.annotation.ParameterName;
@@ -96,9 +98,15 @@ public class ValidateGrant extends AbstractOIDCTokenResponseAction {
/** Lookup function to supply chain revocation lifetime. */
@Nonnull private Function<ProfileRequestContext,Duration> chainRevocationLifetimeLookupStrategy;
+ /** Strategy used to obtain the refresh token lifetime. */
+ @Nonnull private Function<ProfileRequestContext,Duration> refreshTokenChainLifetimeLookupStrategy;
+
/** The RelyingPartyContext to operate on. */
@Nullable private RelyingPartyContext rpCtx;
+ /** Refresh Token lifetime. */
+ @Nullable private Duration refreshTokenChainLifetime;
+
/**
* Constructor.
*
@@ -110,6 +118,7 @@ public class ValidateGrant extends AbstractOIDCTokenResponseAction {
refreshTokensEnabledPredicate = new RefreshTokensEnabledPredicate();
chainRevocationLifetimeLookupStrategy = new DefaultChainRevocationLifetimeLookupStrategy();
((RevocationLifetimeLookupFunction) chainRevocationLifetimeLookupStrategy).setUseActiveProfileOnly(false);
+ refreshTokenChainLifetimeLookupStrategy = new RefreshTokenChainLifetimeLookupFunction();
}
/**
@@ -168,6 +177,18 @@ public class ValidateGrant extends AbstractOIDCTokenResponseAction {
chainRevocationLifetimeLookupStrategy = Constraint.isNotNull(strategy, "Lookup strategy cannot be null");
}
+ /**
+ * Set the strategy used to obtain the refresh token chain lifetime.
+ *
+ * @param strategy lookup strategy
+ */
+ public void setRefreshTokenChainLifetimeLookupStrategy(
+ @Nonnull final Function<ProfileRequestContext,Duration> strategy) {
+ ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+ refreshTokenChainLifetimeLookupStrategy =
+ Constraint.isNotNull(strategy, "Refresh token chain lifetime lookup strategy cannot be null");
+ }
+
/** {@inheritDoc} */
@Override
protected void doInitialize() throws ComponentInitializationException {
@@ -192,7 +213,14 @@ public class ValidateGrant extends AbstractOIDCTokenResponseAction {
ActionSupport.buildEvent(profileRequestContext, IdPEventIds.INVALID_RELYING_PARTY_CTX);
return false;
}
-
+
+ refreshTokenChainLifetime = refreshTokenChainLifetimeLookupStrategy.apply(profileRequestContext);
+ if (refreshTokenChainLifetime == null) {
+ log.warn("{} No lifetime supplied for refresh token", getLogPrefix());
+ ActionSupport.buildEvent(profileRequestContext, IdPEventIds.INVALID_PROFILE_CONFIG);
+ return false;
+ }
+
return true;
}
@@ -270,13 +298,27 @@ public class ValidateGrant extends AbstractOIDCTokenResponseAction {
ActionSupport.buildEvent(profileRequestContext, OidcEventIds.INVALID_GRANT);
return;
}
+ if (refreshTokenClaimsSet.getChainExp() != null
+ && refreshTokenClaimsSet.getChainExp().isBefore(Instant.now())) {
+ log.warn("{} Refresh token chain has expired on {}", getLogPrefix(),
+ refreshTokenClaimsSet.getChainExp());
+ ActionSupport.buildEvent(profileRequestContext, OidcEventIds.INVALID_GRANT);
+ return;
+ }
tokenClaimsSet = refreshTokenClaimsSet;
} catch (final ParseException | DataSealerException e) {
log.warn("{} Unwrapping refresh token failed {}", getLogPrefix(), e.getMessage());
ActionSupport.buildEvent(profileRequestContext, OidcEventIds.INVALID_GRANT);
return;
}
+ if (Instant.now().isAfter(tokenClaimsSet.getAuthenticationTime().plus(refreshTokenChainLifetime))) {
+ log.warn("{} Refresh token chain is expired, the authentication instant was {}", getLogPrefix(),
+ tokenClaimsSet.getAuthenticationTime());
+ ActionSupport.buildEvent(profileRequestContext, OidcEventIds.INVALID_GRANT);
+ return;
+ }
}
+
} else if (GrantType.CLIENT_CREDENTIALS.equals(grant.getType())) {
return;
}
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/security/jwt/claims/impl/ChainExpiryClaimsValidator.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/security/jwt/claims/impl/ChainExpiryClaimsValidator.java
new file mode 100644
index 00000000..e9a06dae
--- /dev/null
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/security/jwt/claims/impl/ChainExpiryClaimsValidator.java
@@ -0,0 +1,99 @@
+/*
+ * Licensed to the University Corporation for Advanced Internet Development,
+ * Inc. (UCAID) under one or more contributor license agreements. See the
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID licenses this file to You under the Apache
+ * License, Version 2.0 (the "License"); you may not use this file except in
+ * compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+package net.shibboleth.idp.plugin.oidc.op.security.jwt.claims.impl;
+
+import java.text.ParseException;
+import java.time.Duration;
+import java.time.Instant;
+import java.util.Date;
+
+import javax.annotation.Nonnull;
+
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.nimbusds.jwt.JWTClaimsSet;
+
+import net.shibboleth.idp.plugin.oidc.op.token.support.RefreshTokenClaimsSet;
+import net.shibboleth.oidc.jwt.claims.AbstractClaimsValidator;
+import net.shibboleth.oidc.jwt.claims.JWTValidationException;
+import net.shibboleth.utilities.java.support.annotation.constraint.ThreadSafeAfterInit;
+import net.shibboleth.utilities.java.support.component.ComponentSupport;
+import net.shibboleth.utilities.java.support.logic.Constraint;
+
+/**
+ * Iff a chain expiration time (c_exp) claim is present, verifies that it is ahead of the current time, else the JWT
+ * claims set is rejected. A few minutes of {@code clockSkew} is allowed.
+ */
+ at ThreadSafeAfterInit
+public class ChainExpiryClaimsValidator extends AbstractClaimsValidator {
+
+ /** Class logger. */
+ @Nonnull private final Logger log = LoggerFactory.getLogger(ChainExpiryClaimsValidator.class);
+
+ /**
+ * Positive clock skew adjustment to consider when checking JWT expiration
+ * (Default value: 60 seconds).
+ */
+ @Nonnull private Duration clockSkew;
+
+ /** Constructor.*/
+ public ChainExpiryClaimsValidator() {
+ clockSkew = Duration.ofSeconds(60);
+ }
+
+ /**
+ * Set the clock skew.
+ *
+ * @param skew clock skew to set
+ */
+ public void setClockSkew(@Nonnull final Duration skew) {
+ ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+
+ clockSkew = Constraint.isNotNull(skew, "Clock skew cannot be null");
+ }
+
+
+ /** {@inheritDoc} */
+ @Override
+ public void doValidate(@Nonnull final JWTClaimsSet claims, @Nonnull final ProfileRequestContext context)
+ throws JWTValidationException {
+
+ final Instant now = Instant.now();
+
+ final Date exp;
+ try {
+ exp = claims.getDateClaim(RefreshTokenClaimsSet.KEY_CHAIN_EXPIRATION_TIME);
+ } catch (final ParseException e) {
+ throw new JWTValidationException("Unexpected contents on '"
+ + RefreshTokenClaimsSet.KEY_CHAIN_EXPIRATION_TIME + "' claim");
+ }
+ if (exp != null) {
+ final Instant expInstant = exp.toInstant();
+ final Instant expirationPlusSkew = expInstant.plus(clockSkew);
+
+ if (now.isAfter(expirationPlusSkew)) {
+ throw new JWTValidationException("Expired JWT by the chain expiration");
+ }
+ }
+
+ }
+
+}
diff --git a/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/service/relying-party/postconfig.xml b/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/service/relying-party/postconfig.xml
index 856d6d75..491efeb4 100644
--- a/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/service/relying-party/postconfig.xml
+++ b/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/service/relying-party/postconfig.xml
@@ -270,9 +270,13 @@
</property>
</bean>
</property>
- <property name="refreshTokenLifetimeLookupStrategy">
- <bean parent="shibboleth.MDDrivenDurationProperty" p:propertyName="refreshTokenLifetime"
- p:defaultValue="%{idp.oidc.refreshToken.defaultLifetime:PT2H}" />
+ <property name="refreshTokenTimeoutLookupStrategy">
+ <bean parent="shibboleth.MDDrivenDurationProperty" p:propertyName="refreshTokenTimeout"
+ p:defaultValue="%{idp.oidc.refreshToken.defaultTimeout:PT2H}" />
+ </property>
+ <property name="refreshTokenChainLifetimeLookupStrategy">
+ <bean parent="shibboleth.MDDrivenDurationProperty" p:propertyName="refreshTokenChainLifetime"
+ p:defaultValue="%{idp.oidc.refreshToken.defaultChainLifetime:PT2H}" />
</property>
<property name="includeIssuerInResponsePredicate">
<bean class="net.shibboleth.utilities.java.support.logic.PredicateSupport" factory-method="fromFunction">
@@ -412,9 +416,13 @@
</property>
</bean>
</property>
- <property name="refreshTokenLifetimeLookupStrategy">
- <bean parent="shibboleth.MDDrivenDurationProperty" p:propertyName="refreshTokenLifetime"
- p:defaultValue="%{idp.oidc.refreshToken.defaultLifetime:PT2H}" />
+ <property name="refreshTokenTimeoutLookupStrategy">
+ <bean parent="shibboleth.MDDrivenDurationProperty" p:propertyName="refreshTokenTimeout"
+ p:defaultValue="%{idp.oidc.refreshToken.defaultTimeout:PT2H}" />
+ </property>
+ <property name="refreshTokenChainLifetimeLookupStrategy">
+ <bean parent="shibboleth.MDDrivenDurationProperty" p:propertyName="refreshTokenChainLifetime"
+ p:defaultValue="%{idp.oidc.refreshToken.defaultChainLifetime:PT2H}" />
</property>
<property name="refreshTokenClaimsSetManipulationStrategyLookupStrategy">
<bean parent="shibboleth.MDDrivenBeanProperty" p:propertyName="refreshTokenClaimsSetManipulationStrategy"
@@ -529,6 +537,10 @@
class="net.shibboleth.oidc.security.jwt.claims.impl.ExpiryClaimsValidator"
p:clockSkew="%{idp.policy.clockSkew:PT1M}" />
+ <bean id="ChainExpiryClaimsValidator"
+ class="net.shibboleth.idp.plugin.oidc.op.security.jwt.claims.impl.ChainExpiryClaimsValidator"
+ p:clockSkew="%{idp.policy.clockSkew:PT1M}" />
+
<bean id="NotBeforeClaimsValidator"
class="net.shibboleth.oidc.security.jwt.claims.impl.NotBeforeClaimsValidator"
p:clockSkew="%{idp.policy.clockSkew:PT1M}" />
@@ -626,6 +638,7 @@
<util:list id="IntrospectionClaimsValidators" value-type="net.shibboleth.oidc.jwt.claims.ClaimsValidator">
<ref bean="RequiredClaimsValidator" />
<ref bean="ExpiryClaimsValidator" />
+ <ref bean="ChainExpiryClaimsValidator" />
<ref bean="NotBeforeClaimsValidator" />
<ref bean="SelfIssuedClaimsValidator" />
<!-- For issued tokens, ensure that the requester is either the client_id or the audience. -->
@@ -645,6 +658,7 @@
<util:list id="RevocationClaimsValidators" value-type="net.shibboleth.oidc.jwt.claims.ClaimsValidator">
<ref bean="RequiredClaimsValidator" />
<ref bean="ExpiryClaimsValidator" />
+ <ref bean="ChainExpiryClaimsValidator" />
<ref bean="SelfIssuedClaimsValidator" />
<!-- For issued tokens, ensure that the requester is either the client_id or the audience. -->
<bean class="net.shibboleth.oidc.security.jwt.claims.impl.ChainingJWTClaimsValidator" p:requireAll="false">
diff --git a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/AbstractOidcApiFlowTest.java b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/AbstractOidcApiFlowTest.java
index bee0730c..ccb5b534 100644
--- a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/AbstractOidcApiFlowTest.java
+++ b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/AbstractOidcApiFlowTest.java
@@ -24,6 +24,7 @@ import java.security.PrivateKey;
import java.security.interfaces.ECPrivateKey;
import java.time.Instant;
import java.util.Collection;
+import java.util.Date;
import com.nimbusds.jose.Algorithm;
import com.nimbusds.jose.JOSEException;
@@ -41,9 +42,8 @@ import com.nimbusds.oauth2.sdk.token.BearerAccessToken;
import com.nimbusds.oauth2.sdk.token.RefreshToken;
import com.nimbusds.openid.connect.sdk.claims.ClaimsSet;
-import net.shibboleth.idp.plugin.oidc.op.profile.impl.BaseOIDCResponseActionTest;
import net.shibboleth.idp.plugin.oidc.op.token.support.AccessTokenClaimsSet;
-import net.shibboleth.idp.plugin.oidc.op.token.support.TokenClaimsSet;
+import net.shibboleth.idp.plugin.oidc.op.token.support.RefreshTokenClaimsSet;
import net.shibboleth.utilities.java.support.component.ComponentInitializationException;
import net.shibboleth.utilities.java.support.security.DataSealerException;
@@ -65,22 +65,31 @@ public class AbstractOidcApiFlowTest extends AbstractOidcFlowTest {
protected RefreshToken buildRefreshToken(final String clientId, final String subject, final Scope scope,
final ClaimsSet userInfoDeliverySet, final String id, final String rootId)
throws URISyntaxException, NoSuchAlgorithmException, DataSealerException, ComponentInitializationException {
+ return buildRefreshToken(clientId, subject, scope, userInfoDeliverySet, id, rootId, Instant.now(), null);
+ }
+
+ protected RefreshToken buildRefreshToken(final String clientId, final String subject, final Scope scope,
+ final ClaimsSet userInfoDeliverySet, final String id, final String rootId, final Instant authTime,
+ final Instant chainExp)
+ throws URISyntaxException, NoSuchAlgorithmException, DataSealerException, ComponentInitializationException {
final String jti = id == null ? idGenerator.generateIdentifier() : id;
- final TokenClaimsSet claims = new AccessTokenClaimsSet.Builder()
- .setJWTID(jti)
- .setClientID(new ClientID(clientId))
- .setIssuer("https://op.example.org")
- .setPrincipal("jdoe")
- .setSubject(subject)
- .setIssuedAt(Instant.now())
- .setExpiresAt(Instant.now().plusSeconds(30))
- .setAuthenticationTime(Instant.now())
- .setRedirectURI(new URI("https://example.org/cb"))
- .setScope(scope)
- .setDlClaimsUI(userInfoDeliverySet)
- .setRootTokenIdentifier(rootId)
- .build();
- return new RefreshToken(claims.serialize(getDataSealer()));
+ final RefreshTokenClaimsSet.Builder builder = new RefreshTokenClaimsSet.Builder();
+ builder.setJWTID(jti)
+ .setClientID(new ClientID(clientId))
+ .setIssuer("https://op.example.org")
+ .setPrincipal("jdoe")
+ .setSubject(subject)
+ .setIssuedAt(Instant.now())
+ .setExpiresAt(Instant.now().plusSeconds(30))
+ .setAuthenticationTime(authTime)
+ .setRedirectURI(new URI("https://example.org/cb"))
+ .setScope(scope)
+ .setDlClaimsUI(userInfoDeliverySet)
+ .setRootTokenIdentifier(rootId);
+ if (chainExp != null) {
+ builder.addCustomClaim(RefreshTokenClaimsSet.KEY_CHAIN_EXPIRATION_TIME, Date.from(chainExp));
+ }
+ return new RefreshToken(builder.build().serialize(getDataSealer()));
}
protected BearerAccessToken buildLegacyToken(final String clientId, final String subject, final Scope scope,
diff --git a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/IntrospectionFlowTest.java b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/IntrospectionFlowTest.java
index 442d7825..dafab8ca 100644
--- a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/IntrospectionFlowTest.java
+++ b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/IntrospectionFlowTest.java
@@ -171,6 +171,47 @@ public class IntrospectionFlowTest extends AbstractOidcClientAuthenticationFlowT
Assert.assertNull(resp.getAudience());
}
+ @Test
+ public void testSuccessWithRefreshToken() throws IOException, NoSuchAlgorithmException, URISyntaxException,
+ DataSealerException, ComponentInitializationException {
+ final String rootId = idGenerator.generateIdentifier();
+ final String jti = idGenerator.generateIdentifier();
+ storeMetadata(storageService, clientId, clientSecret, scope);
+ setBasicAuth(clientId, clientSecret);
+ setHttpFormRequest("POST", Map.of(
+ "token",
+ buildRefreshToken(clientId, "sub", Scope.parse("openid"), null, jti, rootId).toJSONObject()
+ .getAsString("refresh_token"),
+ "token_type",
+ "refresh_token"));
+ final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+ final TokenIntrospectionSuccessResponse resp =
+ parseSuccessResponse(result, TokenIntrospectionSuccessResponse.class);
+ Assert.assertTrue(resp.isActive());
+ Assert.assertEquals(resp.getClientID().getValue(), clientId);
+ Assert.assertEquals(resp.getScope(), Scope.parse("openid"));
+ Assert.assertNull(resp.getAudience());
+ }
+
+ @Test
+ public void testFailureWithChainExpiredRefreshToken() throws IOException, NoSuchAlgorithmException,
+ URISyntaxException, DataSealerException, ComponentInitializationException {
+ final String rootId = idGenerator.generateIdentifier();
+ final String jti = idGenerator.generateIdentifier();
+ storeMetadata(storageService, clientId, clientSecret, scope);
+ setBasicAuth(clientId, clientSecret);
+ setHttpFormRequest("POST", Map.of(
+ "token",
+ buildRefreshToken(clientId, "sub", Scope.parse("openid"), null, jti, rootId, Instant.now(), Instant.now().minusSeconds(120)).toJSONObject()
+ .getAsString("refresh_token"),
+ "token_type",
+ "refresh_token"));
+ final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+ final TokenIntrospectionSuccessResponse resp =
+ parseSuccessResponse(result, TokenIntrospectionSuccessResponse.class);
+ Assert.assertFalse(resp.isActive());
+ }
+
@Test
public void testRevokedSingleToken() throws IOException, NoSuchAlgorithmException, URISyntaxException, DataSealerException,
ComponentInitializationException {
diff --git a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/RevocationFlowTest.java b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/RevocationFlowTest.java
index 84f0438c..6dd6f8e1 100644
--- a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/RevocationFlowTest.java
+++ b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/RevocationFlowTest.java
@@ -164,6 +164,21 @@ public class RevocationFlowTest extends AbstractOidcClientAuthenticationFlowTest
Assert.assertFalse(revocationCache.isRevoked(RevocationCacheContexts.AUTHORIZATION_CODE, rootId));
}
+ @Test
+ public void testSuccessSingleChainExpiredRefreshToken_notRevoked() throws IOException, NoSuchAlgorithmException,
+ URISyntaxException, DataSealerException, ComponentInitializationException {
+ final String id = idGenerator.generateIdentifier();
+ final String rootId = idGenerator.generateIdentifier();
+ setBasicAuth(clientIdSingle, clientSecret);
+ storeMetadata(storageService, clientIdSingle, clientSecret, scope);
+ setHttpFormRequest("POST", Collections.singletonMap("token", super.buildRefreshToken(clientIdSingle, "sub",
+ Scope.parse("openid"), null, id, rootId,Instant.now(), Instant.now().minusSeconds(120)).toJSONObject().getAsString("refresh_token")));
+ final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+ parseSuccessResponse(result, OAuth2RevocationSuccessResponse.class);
+ Assert.assertFalse(revocationCache.isRevoked(RevocationCacheContexts.SINGLE_ACCESS_OR_REFRESH_TOKENS, id));
+ Assert.assertFalse(revocationCache.isRevoked(RevocationCacheContexts.AUTHORIZATION_CODE, rootId));
+ }
+
@Test
public void testSuccessWithSamlMetadata() throws NoSuchAlgorithmException, URISyntaxException,
DataSealerException, ComponentInitializationException {
diff --git a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/TokenFlowTest.java b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/TokenFlowTest.java
index 1abf72c6..d8ed1599 100644
--- a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/TokenFlowTest.java
+++ b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/TokenFlowTest.java
@@ -24,6 +24,7 @@ import java.text.ParseException;
import java.time.Duration;
import java.time.Instant;
import java.util.Collection;
+import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@@ -568,21 +569,27 @@ public class TokenFlowTest extends AbstractOidcClientAuthenticationFlowTest {
protected TokenClaimsSet buildDefaultClaimsSet(final String clientId, final Collection<String> aud)
throws Exception {
- final Instant now = Instant.now();
- final AuthorizeCodeClaimsSet.Builder builder = new AuthorizeCodeClaimsSet.Builder();
- builder.setJWTID(new SecureRandomIdentifierGenerationStrategy())
- .setClientID(new ClientID(clientId))
- .setIssuer(issuer)
- .setPrincipal("jdoe")
- .setSubject("mock")
- .setIssuedAt(now)
- .setExpiresAt(now.plusSeconds(100))
- .setAuthenticationTime(now)
- .setRedirectURI(new URI(redirectUri))
- .setAudience(aud)
- .setScope(scope == null ? new Scope() : scope);
- return builder.build();
- }
+ return buildDefaultClaimsSet(clientId, aud, Instant.now());
+ }
+
+ protected TokenClaimsSet buildDefaultClaimsSet(final String clientId, final Collection<String> aud,
+ final Instant authTime) throws Exception {
+ final Instant now = Instant.now();
+ final AuthorizeCodeClaimsSet.Builder builder = new AuthorizeCodeClaimsSet.Builder();
+ builder.setJWTID(new SecureRandomIdentifierGenerationStrategy())
+ .setClientID(new ClientID(clientId))
+ .setIssuer(issuer)
+ .setPrincipal("jdoe")
+ .setSubject("mock")
+ .setIssuedAt(now)
+ .setExpiresAt(now.plusSeconds(100))
+ .setAuthenticationTime(authTime)
+ .setRedirectURI(new URI(redirectUri))
+ .setAudience(aud)
+ .setScope(scope == null ? new Scope() : scope);
+ return builder.build();
+ }
+
protected String buildRefreshToken(final String clientId, final String id, final String rootId,
final Collection<String> aud, final String... consentedClaims) throws Exception {
final TokenClaimsSet acClaims = buildDefaultClaimsSet(clientId, aud);
@@ -594,6 +601,25 @@ public class TokenFlowTest extends AbstractOidcClientAuthenticationFlowTest {
return new RefreshToken(rtClaims.serialize(getDataSealer())).getValue();
}
+ protected String buildRefreshToken(final String clientId, final String id, final String rootId,
+ final Collection<String> aud, final Instant authTime, final Instant chainExp,
+ final String... consentedClaims) throws Exception {
+ return buildRefreshToken(clientId, id, rootId, aud, authTime, chainExp, Instant.now().plus(Duration.ofHours(1)),
+ consentedClaims);
+ }
+
+ protected String buildRefreshToken(final String clientId, final String id, final String rootId,
+ final Collection<String> aud, final Instant authTime, final Instant chainExp, final Instant tokenExp,
+ final String... consentedClaims) throws Exception {
+ final TokenClaimsSet acClaims = buildDefaultClaimsSet(clientId, aud, authTime);
+ final RefreshTokenClaimsSet rtClaims = new RefreshTokenClaimsSet.Builder(acClaims, Instant.now(), tokenExp)
+ .setRootTokenIdentifier(rootId)
+ .setJWTID(id)
+ .addCustomClaim(RefreshTokenClaimsSet.KEY_CHAIN_EXPIRATION_TIME, Date.from(chainExp))
+ .build();
+ return new RefreshToken(rtClaims.serialize(getDataSealer())).getValue();
+ }
+
protected String buildRefreshTokenWithSid(final String clientId, final String id, final String rootId,
final Collection<String> aud, final String sid, final String... consentedClaims) throws Exception {
final TokenClaimsSet acClaims = buildDefaultClaimsSet(clientId, aud);
@@ -847,6 +873,42 @@ public class TokenFlowTest extends AbstractOidcClientAuthenticationFlowTest {
Assert.assertFalse(revocationCache.isRevoked(RevocationCacheContexts.AUTHORIZATION_CODE, rootId));
}
+ @Test
+ public void testChainExpiredRefreshTokenGrant() throws Exception {
+ final String id = idGenerator.generateIdentifier();
+ final String rootId = idGenerator.generateIdentifier();
+
+ initializeGrantAndRequest(clientId, createRequestParameters(redirectUri, "refresh_token",
+ buildRefreshToken(clientId, id, rootId, null, Instant.now(), Instant.now()), clientId));
+ final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+ assertErrorCode(result, OAuth2Error.INVALID_GRANT_CODE);
+ }
+
+ @Test
+ public void testAuthTimeExpiredRefreshTokenGrant() throws Exception {
+ final String id = idGenerator.generateIdentifier();
+ final String rootId = idGenerator.generateIdentifier();
+
+ initializeGrantAndRequest(clientId, createRequestParameters(redirectUri, "refresh_token",
+ buildRefreshToken(clientId, id, rootId, null, Instant.now().minus(Duration.ofHours(2)),
+ Instant.now().plus(Duration.ofHours(5))), clientId));
+ final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+ assertErrorCode(result, OAuth2Error.INVALID_GRANT_CODE);
+ }
+
+ @Test
+ public void testExpiredRefreshTokenGrant() throws Exception {
+ final String id = idGenerator.generateIdentifier();
+ final String rootId = idGenerator.generateIdentifier();
+
+ initializeGrantAndRequest(clientId, createRequestParameters(redirectUri, "refresh_token",
+ buildRefreshToken(clientId, id, rootId, null, Instant.now(),
+ Instant.now().plus(Duration.ofHours(5)), Instant.now()),
+ clientId));
+ final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+ assertErrorCode(result, OAuth2Error.INVALID_GRANT_CODE);
+ }
+
@Test
public void testValidRefreshTokenGrant_idTokenIssuanceDisabled() throws Exception {
final String clientId = clientIdNoIdTokenViaRefreshToken;
diff --git a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/SetRefreshTokenToResponseContextTest.java b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/SetRefreshTokenToResponseContextTest.java
index 7be83c93..7ce13b18 100644
--- a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/SetRefreshTokenToResponseContextTest.java
+++ b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/SetRefreshTokenToResponseContextTest.java
@@ -36,6 +36,8 @@ import java.security.NoSuchAlgorithmException;
import java.text.ParseException;
import java.time.Duration;
import java.time.Instant;
+import java.time.temporal.ChronoUnit;
+import java.util.Date;
import java.util.Map;
import java.util.function.BiFunction;
import java.util.function.Function;
@@ -178,6 +180,41 @@ public class SetRefreshTokenToResponseContextTest extends BaseOIDCResponseAction
Assert.assertNotNull(rt);
Assert.assertNotEquals(rt.getID(), jit);
Assert.assertEquals(rt.getRootTokenIdentifier(), rootTokenId);
+ Assert.assertTrue(rt.getChainExp().isAfter(Instant.now()));
+ Assert.assertFalse(revocationCache.isRevoked(RevocationCacheContexts.SINGLE_ACCESS_OR_REFRESH_TOKENS, jit));
+ }
+
+ @Test
+ public void testSuccessViaRefresh_existingChainExp() throws ComponentInitializationException, NoSuchAlgorithmException, URISyntaxException,
+ ParseException, DataSealerException {
+ final String rootTokenId = new SecureRandomIdentifierGenerationStrategy().generateIdentifier();
+ final Instant chainExp = Instant.now().plusSeconds(300).truncatedTo(ChronoUnit.SECONDS);
+ final TokenClaimsSet claims = new RefreshTokenClaimsSet.Builder()
+ .setJWTID(idGenerator)
+ .setClientID(new ClientID())
+ .setIssuer("issuer")
+ .setPrincipal("userPrin")
+ .setSubject("subject")
+ .setIssuedAt(Instant.now())
+ .setExpiresAt(Instant.now())
+ .setAuthenticationTime(Instant.now())
+ .setRedirectURI(new URI("http://example.com"))
+ .setScope(new Scope())
+ .setACR(new ACR("0"))
+ .setRootTokenIdentifier(rootTokenId)
+ .addCustomClaim(RefreshTokenClaimsSet.KEY_CHAIN_EXPIRATION_TIME, Date.from(chainExp))
+ .build();
+ final String jit = claims.getID();
+ respCtx.setAuthorizationGrantClaimsSet(claims);
+ final Event event = action.execute(requestCtx);
+ ActionTestingSupport.assertProceedEvent(event);
+ Assert.assertNotNull(respCtx.getRefreshToken());
+ final RefreshTokenClaimsSet rt =
+ RefreshTokenClaimsSet.parse(respCtx.getRefreshToken().getValue(), getDataSealer());
+ Assert.assertNotNull(rt);
+ Assert.assertNotEquals(rt.getID(), jit);
+ Assert.assertEquals(rt.getRootTokenIdentifier(), rootTokenId);
+ Assert.assertEquals(rt.getChainExp(), chainExp);
Assert.assertFalse(revocationCache.isRevoked(RevocationCacheContexts.SINGLE_ACCESS_OR_REFRESH_TOKENS, jit));
}
@@ -210,6 +247,7 @@ public class SetRefreshTokenToResponseContextTest extends BaseOIDCResponseAction
Assert.assertNotNull(rt);
Assert.assertNotEquals(rt.getID(), jit);
Assert.assertEquals(rt.getRootTokenIdentifier(), rootTokenId);
+ Assert.assertTrue(rt.getChainExp().isAfter(Instant.now()));
Assert.assertFalse(revocationCache.isRevoked(RevocationCacheContexts.SINGLE_ACCESS_OR_REFRESH_TOKENS, jit));
Assert.assertNotNull(rt.getClaimsSet().getClaim("custom_claim"));
Assert.assertEquals(rt.getClaimsSet().getStringClaim("custom_claim"), "custom_value");
@@ -244,6 +282,7 @@ public class SetRefreshTokenToResponseContextTest extends BaseOIDCResponseAction
Assert.assertNotNull(rt);
Assert.assertNotEquals(rt.getID(), jit);
Assert.assertEquals(rt.getRootTokenIdentifier(), rootTokenId);
+ Assert.assertTrue(rt.getChainExp().isAfter(Instant.now()));
Assert.assertTrue(revocationCache.isRevoked(RevocationCacheContexts.SINGLE_ACCESS_OR_REFRESH_TOKENS, jit));
}
diff --git a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/ValidateGrantTest.java b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/ValidateGrantTest.java
index 05ceb6b9..124ac739 100644
--- a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/ValidateGrantTest.java
+++ b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/ValidateGrantTest.java
@@ -94,8 +94,18 @@ public class ValidateGrantTest extends BaseOIDCResponseActionTest {
init(refreshTokensEnabled, new MockRevocationCache(false, true), null);
}
+ private void init(final Instant authenticationTime) throws Exception {
+ init(true, new MockRevocationCache(false, true), null, authenticationTime);
+ }
+
private void init(boolean refreshTokensEnabled, final RevocationCache revocationCache,
final Function<ProfileRequestContext, Duration> revocationLifetimeLookup) throws Exception {
+ init(refreshTokensEnabled, revocationCache, revocationLifetimeLookup, Instant.now());
+ }
+
+ private void init(boolean refreshTokensEnabled, final RevocationCache revocationCache,
+ final Function<ProfileRequestContext, Duration> revocationLifetimeLookup,
+ final Instant authenticationTime) throws Exception {
final Instant now = Instant.now();
rootTokenId = "mockId" + now.toEpochMilli();
acClaims = new AuthorizeCodeClaimsSet.Builder()
@@ -106,7 +116,7 @@ public class ValidateGrantTest extends BaseOIDCResponseActionTest {
.setSubject("subject")
.setIssuedAt(Instant.now())
.setExpiresAt(Instant.now().plusSeconds(100))
- .setAuthenticationTime(Instant.now())
+ .setAuthenticationTime(authenticationTime)
.setRedirectURI(new URI("http://example.com"))
.setScope(new Scope())
.build();
@@ -235,6 +245,17 @@ public class ValidateGrantTest extends BaseOIDCResponseActionTest {
Assert.assertNotNull(arc.getAuthorizationGrantClaimsSet());
}
+ @Test
+ public void testRefreshTokenChainExpired() throws Exception {
+ init(Instant.now().minus(Duration.ofHours(2)));
+ final TokenRequest req = new TokenRequest(callback, new ClientID(clientId), rfGrant);
+ profileRequestCtx.getInboundMessageContext().setMessage(req);
+ ActionTestingSupport.assertEvent(action.execute(requestCtx), OidcEventIds.INVALID_GRANT);
+ final OIDCAuthenticationResponseContext arc =
+ profileRequestCtx.getOutboundMessageContext().getSubcontext(OIDCAuthenticationResponseContext.class);
+ Assert.assertNull(arc.getAuthorizationGrantClaimsSet());
+ }
+
@Test
public void testRefreshTokenNotEnabled() throws Exception {
init(false);
--
To stop receiving notification emails like this one, please contact
the administrator of this repository.
More information about the commits
mailing list