[java-idp-oidc] branch main updated: JOIDC-90 - Revocation of individual tokens
Henri Mikkonen
henri.mikkonen at iki.fi
Tue May 31 11:39:37 UTC 2022
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=09d5b6f137d840476bf682a5137f7cf57fa2a8c1
The following commit(s) were added to refs/heads/main by this push:
new 09d5b6f1 JOIDC-90 - Revocation of individual tokens
09d5b6f1 is described below
commit 09d5b6f137d840476bf682a5137f7cf57fa2a8c1
Author: Henri Mikkonen <henri.mikkonen at iki.fi>
AuthorDate: Tue May 31 14:36:22 2022 +0300
JOIDC-90 - Revocation of individual tokens
https://shibboleth.atlassian.net/browse/JOIDC-90
Added a new revocation cache context for individual tokens. In the token
claims set, new claim (root_jti) refers to the root token/grant for the
chain of tokens. With code flow, that value is the ID of authorization
code claims set. With client_credentials grant, it's the ID of the initial
access token. From now on, a new jti (JWT id) is created for new tokens.
---
.../oidc/op/storage/RevocationCacheContexts.java | 8 +-
.../op/token/support/AccessTokenClaimsSet.java | 1 +
.../oidc/op/token/support/TokenClaimsSet.java | 42 ++++++-
.../op/oauth2/profile/impl/BuildAccessToken.java | 22 ++--
.../oidc/op/oauth2/profile/impl/RevokeToken.java | 138 +++++++++++++++++++--
.../impl/SetRefreshTokenToResponseContext.java | 38 +++++-
.../DefaultRootTokenIdentifierLookupStrategy.java | 64 ++++++++++
.../op/oauth2/profile/impl/RevokeTokenTest.java | 131 +++++++++++++++++--
.../impl/SetRefreshTokenToResponseContextTest.java | 37 +++++-
9 files changed, 446 insertions(+), 35 deletions(-)
diff --git a/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/storage/RevocationCacheContexts.java b/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/storage/RevocationCacheContexts.java
index 99e6890e..49be4745 100644
--- a/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/storage/RevocationCacheContexts.java
+++ b/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/storage/RevocationCacheContexts.java
@@ -33,7 +33,13 @@ public final class RevocationCacheContexts {
*/
@Nonnull @NotEmpty public static final String AUTHORIZATION_CODE =
RevocationCacheContexts.class.getName() + ".AUTHORIZATION_CODE";
-
+
+ /**
+ * ID of context for revoking single access or refresh tokens.
+ */
+ @Nonnull @NotEmpty public static final String SINGLE_ACCESS_OR_REFRESH_TOKENS =
+ RevocationCacheContexts.class.getName() + ".SINGLE_ACCESS_OR_REFRESH_TOKENS";
+
/**
* ID of context for revoking access tokens issued for the dynamic client registration.
*/
diff --git a/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/token/support/AccessTokenClaimsSet.java b/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/token/support/AccessTokenClaimsSet.java
index 0bce7967..7fcc8804 100644
--- a/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/token/support/AccessTokenClaimsSet.java
+++ b/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/token/support/AccessTokenClaimsSet.java
@@ -230,6 +230,7 @@ public final class AccessTokenClaimsSet extends TokenClaimsSet {
setClaimsRequest(existing.getClaimsRequest());
setConsentedClaims(existing.getConsentedClaims());
setConsentEnabled(existing.isConsentEnabled());
+ setRootTokenIdentifier(existing.getRootTokenIdentifier());
}
/**
diff --git a/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/token/support/TokenClaimsSet.java b/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/token/support/TokenClaimsSet.java
index ce81beb2..286a3063 100644
--- a/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/token/support/TokenClaimsSet.java
+++ b/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/token/support/TokenClaimsSet.java
@@ -132,7 +132,10 @@ public class TokenClaimsSet {
/** Custom claim name for sealed claims embedded inside JWT. */
@Nonnull @NotEmpty public static final String KEY_SEALED_FOR_OP = "for_op";
-
+
+ /** Identifier for the root token in the chain. */
+ @Nonnull @NotEmpty public static final String KEY_ROOT_JTI = "root_jti";
+
/** Claims set for the claim. */
@Nullable private JWTClaimsSet tokenClaimsSet;
@@ -624,6 +627,21 @@ public class TokenClaimsSet {
return null;
}
+ /**
+ * Get the root token identifier.
+ *
+ * @return the root token identifier.
+ *
+ * @since 3.2.0
+ */
+ @Nullable public String getRootTokenIdentifier() {
+ Constraint.isNotNull(tokenClaimsSet, "JWTClaimsSet cannot be null");
+ if (tokenClaimsSet.getClaim(KEY_ROOT_JTI) == null) {
+ return null;
+ }
+ return (String) tokenClaimsSet.getClaim(KEY_ROOT_JTI);
+ }
+
/**
* Abstract builder to extend builders from that are instantiating claims sets extending TokenClaimsSet.
*
@@ -699,6 +717,9 @@ public class TokenClaimsSet {
/** Extends the token with custom claims. */
@Nonnull protected Map<String,Object> customClaims;
+ /** Root token identifier. */
+ @Nullable protected String rootTokenId;
+
/** Default constructor. */
protected Builder() {
audience = Collections.emptyList();
@@ -747,7 +768,8 @@ public class TokenClaimsSet {
.claim(KEY_DELIVERY_CLAIMS_USERINFO, dlClaimsUI == null ? null : dlClaimsUI.toJSONObject())
.claim(KEY_CONSENTED_CLAIMS, consentedClaims)
.claim(KEY_CODE_CHALLENGE, codeChallenge)
- .claim(KEY_CONSENT_ENABLED, consentEnabled);
+ .claim(KEY_CONSENT_ENABLED, consentEnabled)
+ .claim(KEY_ROOT_JTI, rootTokenId);
customClaims.forEach((n,v) -> {
if (n != null) {
@@ -1106,7 +1128,21 @@ public class TokenClaimsSet {
});
return this;
}
-
+
+ /**
+ * Set root token identifier.
+ *
+ * @param id root token identifier
+ *
+ * @return the builder
+ *
+ * @since 3.2.0
+ */
+ public Builder<T> setRootTokenIdentifier(@Nullable final String id) {
+ rootTokenId = id;
+ return this;
+ }
+
/**
* Builds claims set.
*
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/BuildAccessToken.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/BuildAccessToken.java
index e36a273b..0c917ddd 100644
--- a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/BuildAccessToken.java
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/BuildAccessToken.java
@@ -72,6 +72,7 @@ import net.shibboleth.utilities.java.support.component.ComponentInitializationEx
import net.shibboleth.utilities.java.support.component.ComponentSupport;
import net.shibboleth.utilities.java.support.logic.Constraint;
import net.shibboleth.utilities.java.support.logic.FunctionSupport;
+import net.shibboleth.utilities.java.support.primitive.StringSupport;
import net.shibboleth.utilities.java.support.security.DataSealer;
import net.shibboleth.utilities.java.support.security.DataSealerException;
import net.shibboleth.utilities.java.support.security.IdentifierGenerationStrategy;
@@ -357,7 +358,14 @@ public class BuildAccessToken extends AbstractOIDCResponseAction {
ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
return false;
}
-
+
+ idGenerator = idGeneratorLookupStrategy.apply(profileRequestContext);
+ if (idGenerator == null) {
+ log.error("{} No identifier generation strategy", getLogPrefix());
+ ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
+ return false;
+ }
+
if (tokenClaimsSet == null) {
/*
* Typically this path applies when the client_credentials grant is used.
@@ -372,13 +380,6 @@ public class BuildAccessToken extends AbstractOIDCResponseAction {
return false;
}
- idGenerator = idGeneratorLookupStrategy.apply(profileRequestContext);
- if (idGenerator == null) {
- log.error("{} No identifier generation strategy", getLogPrefix());
- ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
- return false;
- }
-
if (profileRequestContext.getInboundMessageContext() != null
&& profileRequestContext.getInboundMessageContext().getMessage() instanceof AuthenticationRequest) {
authenticationRequest =
@@ -455,6 +456,11 @@ public class BuildAccessToken extends AbstractOIDCResponseAction {
dateExp);
// Add additional bits.
builder.setAudience(responseCtx.getAudience());
+ builder.setJWTID(idGenerator);
+ // Set root token identifier to contain jit from the claims set used for building the new token
+ if (StringSupport.trimOrNull(tokenClaimsSet.getRootTokenIdentifier()) == null) {
+ builder.setRootTokenIdentifier(tokenClaimsSet.getID());
+ }
} else {
final OIDCAuthenticationResponseConsentContext consentCtx =
consentContextLookupStrategy.apply(profileRequestContext);
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/RevokeToken.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/RevokeToken.java
index 91e7da31..fb7df393 100644
--- a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/RevokeToken.java
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/RevokeToken.java
@@ -17,7 +17,11 @@
package net.shibboleth.idp.plugin.oidc.op.oauth2.profile.impl;
+import java.time.Duration;
+import java.util.function.Function;
+
import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
import org.opensaml.profile.action.ActionSupport;
import org.opensaml.profile.action.EventIds;
@@ -26,19 +30,29 @@ import org.opensaml.storage.RevocationCache;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+import com.nimbusds.jwt.JWTClaimsSet;
+
import net.shibboleth.idp.plugin.oidc.op.oauth2.messaging.context.OAuth2TokenMgmtResponseContext;
+import net.shibboleth.idp.plugin.oidc.op.profile.logic.DefaultRootTokenIdentifierLookupStrategy;
import net.shibboleth.idp.plugin.oidc.op.storage.RevocationCacheContexts;
import net.shibboleth.idp.profile.AbstractProfileAction;
+import net.shibboleth.idp.profile.IdPEventIds;
+import net.shibboleth.oidc.profile.config.navigate.RevocationLifetimeLookupFunction;
+import net.shibboleth.oidc.profile.config.navigate.RevocationMethodLookupFunction;
import net.shibboleth.oidc.profile.core.OidcEventIds;
+import net.shibboleth.oidc.profile.oauth2.config.OAuth2TokenRevocationConfiguration.OAuth2TokenRevocationMethod;
import net.shibboleth.utilities.java.support.annotation.constraint.NonnullAfterInit;
import net.shibboleth.utilities.java.support.component.ComponentInitializationException;
import net.shibboleth.utilities.java.support.component.ComponentSupport;
import net.shibboleth.utilities.java.support.logic.Constraint;
+import net.shibboleth.utilities.java.support.primitive.StringSupport;
/**
- * Action revokes a token. If the token is was derived from an authorization code, the id of the
- * authorization code they are derived from is marked as revoked and so invalidating all
- * tokens based on it.
+ * Action that revokes a single token or the full chain of tokens, depending on the result of the configured lookup
+ * strategy for the revocation method. The full chain of tokens refer to the token that is fed to this action together
+ * with all other tokens related to the same root token identifier. For legacy reasons, if the root token identifier
+ * is not existing in the claims set but the full chain is to be revoked, the JWT identifier (jti) from the claims set
+ * is used as the root token identifier.
*
* @event {@link EventIds#PROCEED_EVENT_ID}
* @event {@link OidcEventIds#REVOCATION_FAILED}
@@ -50,7 +64,34 @@ public class RevokeToken extends AbstractProfileAction {
/** Message revocation cache instance to use. */
@NonnullAfterInit private RevocationCache revocationCache;
-
+
+ /**
+ * Which revocation method should be used when revoking a token.
+ * Supported values are CHAIN and TOKEN. The default is CHAIN.
+ */
+ @Nonnull private Function<ProfileRequestContext,OAuth2TokenRevocationMethod> revocationMethodLookupStrategy;
+
+ /** Lookup function to supply revocation lifetime. */
+ @Nonnull private Function<ProfileRequestContext,Duration> revocationLifetimeLookupStrategy;
+
+ /** Lookup function to supply root token identifier. */
+ @Nonnull private Function<JWTClaimsSet,String> rootTokenIdentifierLookupStrategy;
+
+ /** Revocation method used when revoking a token. */
+ private OAuth2TokenRevocationMethod revocationMethod;
+
+ /** Revocation lifetime to use. */
+ private Duration revocationLifetime;
+
+ /**
+ * Constructor.
+ */
+ public RevokeToken() {
+ revocationMethodLookupStrategy = new RevocationMethodLookupFunction();
+ revocationLifetimeLookupStrategy = new RevocationLifetimeLookupFunction();
+ rootTokenIdentifierLookupStrategy = new DefaultRootTokenIdentifierLookupStrategy();
+ }
+
/**
* Set the revocation cache instance to use.
*
@@ -61,14 +102,66 @@ public class RevokeToken extends AbstractProfileAction {
revocationCache = Constraint.isNotNull(cache, "RevocationCache cannot be null");
}
+ /**
+ * Set strategy for looking up which revocation method should be used when revoking a token.
+ *
+ * @param strategy What to set.
+ */
+ public void setRevocationMethodLookupStrategy(
+ @Nonnull final Function<ProfileRequestContext,OAuth2TokenRevocationMethod> strategy) {
+ revocationMethodLookupStrategy = Constraint.isNotNull(strategy, "Lookup strategy cannot be null");
+ }
+
+ /**
+ * Set a lookup strategy for the revocation lifetime.
+ *
+ * @param strategy What to set.
+ */
+ public void setRevocationLifetimeLookupStrategy(
+ @Nullable final Function<ProfileRequestContext,Duration> strategy) {
+ revocationLifetimeLookupStrategy = Constraint.isNotNull(strategy, "Lookup strategy cannot be null");
+ }
+
+ /**
+ * Set a lookup strategy for the root token identifier.
+ *
+ * @param strategy What to set.
+ */
+ public void setRootTokenIdentifierLookupStrategy(@Nullable final Function<JWTClaimsSet,String> strategy) {
+ rootTokenIdentifierLookupStrategy = Constraint.isNotNull(strategy, "Lookup strategy cannot be null");
+ }
+
/** {@inheritDoc} */
@Override
protected void doInitialize() throws ComponentInitializationException {
super.doInitialize();
if (revocationCache == null) {
- throw new ComponentInitializationException("RevocationCache and DataSealer cannot be null");
+ throw new ComponentInitializationException("RevocationCache cannot be null");
+ }
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected boolean doPreExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
+ if (!super.doPreExecute(profileRequestContext)) {
+ return false;
}
+
+ revocationMethod = revocationMethodLookupStrategy.apply(profileRequestContext);
+ if (revocationMethod == null) {
+ log.error("{} Unable to obtain revocation method to use", getLogPrefix());
+ ActionSupport.buildEvent(profileRequestContext, IdPEventIds.INVALID_PROFILE_CONFIG);
+ return false;
+ }
+
+ revocationLifetime = revocationLifetimeLookupStrategy.apply(profileRequestContext);
+ if (revocationLifetime == null) {
+ log.error("{} Unable to obtain revocation lifetime to use", getLogPrefix());
+ ActionSupport.buildEvent(profileRequestContext, IdPEventIds.INVALID_PROFILE_CONFIG);
+ return false;
+ }
+ return true;
}
/** {@inheritDoc} */
@@ -82,18 +175,39 @@ public class RevokeToken extends AbstractProfileAction {
return;
}
- final String id = ctx.getTokenClaimsSet().getJWTID();
- if (id == null) {
+ final JWTClaimsSet claimsSet = ctx.getTokenClaimsSet();
+ final String jti = claimsSet.getJWTID();
+ if (jti == null) {
log.error("{} No ID found in token claims set (this should be impossible)", getLogPrefix());
return;
}
-
- if (revocationCache.revoke(RevocationCacheContexts.AUTHORIZATION_CODE, id)) {
- log.debug("{} Revoked all tokens based on ID '{}'", getLogPrefix(), id);
+
+ if (OAuth2TokenRevocationMethod.CHAIN.equals(revocationMethod)) {
+ final String rootJti = rootTokenIdentifierLookupStrategy.apply(claimsSet);
+ final String idToRevoke;
+ if (StringSupport.trimOrNull(rootJti) == null) {
+ log.warn("{} No root token identifier returned, using JWT id", getLogPrefix());
+ idToRevoke = jti;
+ } else {
+ idToRevoke = rootJti;
+ }
+ if (revocationCache.revoke(RevocationCacheContexts.AUTHORIZATION_CODE, idToRevoke, revocationLifetime)) {
+ log.debug("{} Revoked all tokens based on ID '{}'", getLogPrefix(), idToRevoke);
+ } else {
+ log.warn("{} Failed to revoke tokens based on ID '{}'", getLogPrefix(), idToRevoke);
+ ActionSupport.buildEvent(profileRequestContext, OidcEventIds.REVOCATION_FAILED);
+ }
+ } else if (OAuth2TokenRevocationMethod.TOKEN.equals(revocationMethod)) {
+ if (revocationCache.revoke(RevocationCacheContexts.SINGLE_ACCESS_OR_REFRESH_TOKENS, jti,
+ revocationLifetime)) {
+ log.debug("{} Revoked the single token with ID '{}'", getLogPrefix(), jti);
+ } else {
+ log.warn("{} Failed to revoke the single token with ID '{}'", getLogPrefix(), jti);
+ ActionSupport.buildEvent(profileRequestContext, OidcEventIds.REVOCATION_FAILED);
+ }
} else {
- log.warn("{} Failed to revoke tokens based on ID '{}'", getLogPrefix(), id);
+ log.error("{} Unrecognized revocation method: {}", getLogPrefix(), revocationMethod);
ActionSupport.buildEvent(profileRequestContext, OidcEventIds.REVOCATION_FAILED);
}
}
-
}
\ No newline at end of file
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 efa73e06..18581fe2 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
@@ -47,8 +47,12 @@ import org.opensaml.profile.action.ActionSupport;
import net.shibboleth.utilities.java.support.annotation.ParameterName;
import net.shibboleth.utilities.java.support.component.ComponentSupport;
import net.shibboleth.utilities.java.support.logic.Constraint;
+import net.shibboleth.utilities.java.support.logic.FunctionSupport;
+import net.shibboleth.utilities.java.support.primitive.StringSupport;
import net.shibboleth.utilities.java.support.security.DataSealer;
import net.shibboleth.utilities.java.support.security.DataSealerException;
+import net.shibboleth.utilities.java.support.security.IdentifierGenerationStrategy;
+import net.shibboleth.utilities.java.support.security.impl.SecureRandomIdentifierGenerationStrategy;
/**
* Action that creates a Refresh Token, and sets it to work context
@@ -75,12 +79,18 @@ public class SetRefreshTokenToResponseContext extends AbstractOIDCResponseAction
/** The strategy used for manipulating the token claims set. */
@Nullable private BiFunction<ProfileRequestContext,Map<String,Object>,Map<String,Object>> manipulationStrategy;
+ /** Strategy used to locate the {@link IdentifierGenerationStrategy} to use. */
+ @Nonnull private Function<ProfileRequestContext,IdentifierGenerationStrategy> idGeneratorLookupStrategy;
+
/** Authorize Code / Refresh Token the refresh token will be based on. */
@Nullable private TokenClaimsSet tokenClaimsSet;
/** Refresh Token lifetime. */
@Nullable private Duration refreshTokenLifetime;
+ /** The generator to use. */
+ @Nullable private IdentifierGenerationStrategy idGenerator;
+
/**
* Constructor.
*
@@ -91,6 +101,7 @@ public class SetRefreshTokenToResponseContext extends AbstractOIDCResponseAction
dataSealer = Constraint.isNotNull(sealer, "DataSealer cannot be null");
tokenClaimsSetManipulationStrategyLookupStrategy =
new RefreshTokenClaimsSetManipulationStrategyLookupFunction();
+ idGeneratorLookupStrategy = FunctionSupport.constant(new SecureRandomIdentifierGenerationStrategy());
}
/**
@@ -120,6 +131,19 @@ public class SetRefreshTokenToResponseContext extends AbstractOIDCResponseAction
Constraint.isNotNull(strategy, "Manipulation strategy lookup strategy cannot be null");
}
+ /**
+ * Set the strategy used to locate the {@link IdentifierGenerationStrategy} to use.
+ *
+ * @param strategy lookup strategy
+ */
+ public void setIdentifierGeneratorLookupStrategy(
+ @Nonnull final Function<ProfileRequestContext,IdentifierGenerationStrategy> strategy) {
+ ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+
+ idGeneratorLookupStrategy =
+ Constraint.isNotNull(strategy, "Identifier generation strategy cannot be null");
+ }
+
/** {@inheritDoc} */
@Override
protected boolean doPreExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
@@ -150,6 +174,13 @@ public class SetRefreshTokenToResponseContext extends AbstractOIDCResponseAction
manipulationStrategy = tokenClaimsSetManipulationStrategyLookupStrategy.apply(profileRequestContext);
+ idGenerator = idGeneratorLookupStrategy.apply(profileRequestContext);
+ if (idGenerator == null) {
+ log.error("{} No identifier generation strategy", getLogPrefix());
+ ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
+ return false;
+ }
+
return true;
}
@@ -157,8 +188,13 @@ public class SetRefreshTokenToResponseContext extends AbstractOIDCResponseAction
@Override
protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
final Instant dateExp = Instant.now().plus(refreshTokenLifetime);
+ final String rootTokenId = StringSupport.trimOrNull(tokenClaimsSet.getRootTokenIdentifier()) == null ?
+ tokenClaimsSet.getID() : tokenClaimsSet.getRootTokenIdentifier();
final RefreshTokenClaimsSet claimsSet =
- new RefreshTokenClaimsSet.Builder(tokenClaimsSet, Instant.now(), dateExp).build();
+ new RefreshTokenClaimsSet.Builder(tokenClaimsSet, Instant.now(), dateExp)
+ .setJWTID(idGenerator)
+ .setRootTokenIdentifier(rootTokenId)
+ .build();
if (manipulationStrategy != null) {
log.debug("{} Manipulation strategy has been set, applying it to the claims set {}", getLogPrefix(),
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/logic/DefaultRootTokenIdentifierLookupStrategy.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/logic/DefaultRootTokenIdentifierLookupStrategy.java
new file mode 100644
index 00000000..8416fd01
--- /dev/null
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/logic/DefaultRootTokenIdentifierLookupStrategy.java
@@ -0,0 +1,64 @@
+/*
+ * 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.profile.logic;
+
+import java.text.ParseException;
+import java.util.function.Function;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.nimbusds.jwt.JWTClaimsSet;
+
+import net.shibboleth.idp.plugin.oidc.op.token.support.TokenClaimsSet;
+import net.shibboleth.utilities.java.support.primitive.StringSupport;
+
+/**
+ * Default lookup function for fetching the root token identifier from the given claims set.
+ */
+public class DefaultRootTokenIdentifierLookupStrategy implements Function<JWTClaimsSet, String> {
+
+ /** Class logger. */
+ @Nonnull
+ private final Logger log = LoggerFactory.getLogger(DefaultRootTokenIdentifierLookupStrategy.class);
+
+ /** {@inheritDoc} */
+ @Override
+ @Nullable
+ public String apply(@Nullable final JWTClaimsSet claimsSet) {
+ if (claimsSet == null) {
+ log.error("The given claims set was null, returning null");
+ return null;
+ }
+ try {
+ final String rootJti = claimsSet.getStringClaim(TokenClaimsSet.KEY_ROOT_JTI);
+ if (StringSupport.trimOrNull(rootJti) != null) {
+ log.debug("Root token identifier found from the claims set");
+ return rootJti;
+ }
+ } catch (final ParseException e) {
+ log.error("Could not parse the root token identifier from the claims set", e);
+ }
+ log.debug("Could not find root token identifier, returning null");
+ return null;
+ }
+
+}
\ No newline at end of file
diff --git a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/RevokeTokenTest.java b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/RevokeTokenTest.java
index 6c4f4fc1..6fed64f8 100644
--- a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/RevokeTokenTest.java
+++ b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/RevokeTokenTest.java
@@ -25,7 +25,6 @@ import org.opensaml.storage.impl.MemoryStorageService;
import org.springframework.webflow.execution.RequestContext;
import org.testng.Assert;
import org.testng.annotations.AfterMethod;
-import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;
import net.shibboleth.idp.plugin.oidc.op.oauth2.messaging.context.OAuth2TokenMgmtResponseContext;
@@ -37,6 +36,7 @@ import net.shibboleth.idp.plugin.oidc.op.token.support.testing.BaseTokenClaimsSe
import net.shibboleth.idp.profile.context.navigate.WebflowRequestContextProfileRequestContextLookup;
import net.shibboleth.idp.profile.testing.ActionTestingSupport;
import net.shibboleth.idp.profile.testing.RequestContextBuilder;
+import net.shibboleth.oidc.profile.oauth2.config.OAuth2TokenRevocationConfiguration.OAuth2TokenRevocationMethod;
import net.shibboleth.utilities.java.support.security.impl.SecureRandomIdentifierGenerationStrategy;
// Checkstyle: ThrowsCount OFF
@@ -61,9 +61,8 @@ public class RevokeTokenTest extends BaseTokenClaimsSetTest {
private ProfileRequestContext prc;
private OAuth2TokenMgmtResponseContext tokenCtx;
-
- @BeforeMethod
- protected void setUp() throws Exception {
+
+ protected void setUp(final OAuth2TokenRevocationMethod method, final String rootTokenId) throws Exception {
storageService = new MemoryStorageService();
storageService.setId("test");
@@ -87,12 +86,19 @@ public class RevokeTokenTest extends BaseTokenClaimsSetTest {
.setRedirectURI(redirectURI)
.setScope(scope).
build();
- atClaimsSet = new AccessTokenClaimsSet.Builder(acClaimsSet, scope, dlClaims, dlClaimsUI, iat, exp).build();
- rfClaimsSet = new RefreshTokenClaimsSet.Builder(acClaimsSet, iat, exp).build();
+ atClaimsSet = new AccessTokenClaimsSet.Builder(acClaimsSet, scope, dlClaims, dlClaimsUI, iat, exp)
+ .setRootTokenIdentifier(rootTokenId)
+ .build();
+ rfClaimsSet = new RefreshTokenClaimsSet.Builder(acClaimsSet, iat, exp)
+ .setRootTokenIdentifier(rootTokenId)
+ .build();
// init action
action = new RevokeToken();
action.setRevocationCache(revocationCache);
+ action.setRevocationMethodLookupStrategy(
+ prc -> method);
+ action.setRevocationLifetimeLookupStrategy(prc -> Duration.ofHours(1));
action.initialize();
src = new RequestContextBuilder().buildRequestContext();
@@ -108,24 +114,131 @@ public class RevokeTokenTest extends BaseTokenClaimsSetTest {
}
@Test
- public void testNoToken() {
+ public void testChain_NoToken() throws Exception {
+ setUp(OAuth2TokenRevocationMethod.CHAIN, null);
ActionTestingSupport.assertProceedEvent(action.execute(src));
}
@Test
- public void testRevokeAccessToken() {
+ public void testChain_RevokeAccessToken() throws Exception {
+ setUp(OAuth2TokenRevocationMethod.CHAIN, null);
Assert.assertFalse(revocationCache.isRevoked(RevocationCacheContexts.AUTHORIZATION_CODE, atClaimsSet.getID()));
+ Assert.assertFalse(revocationCache.isRevoked(RevocationCacheContexts.SINGLE_ACCESS_OR_REFRESH_TOKENS,
+ atClaimsSet.getID()));
tokenCtx.setTokenClaimsSet(atClaimsSet.getClaimsSet());
ActionTestingSupport.assertProceedEvent(action.execute(src));
Assert.assertTrue(revocationCache.isRevoked(RevocationCacheContexts.AUTHORIZATION_CODE, atClaimsSet.getID()));
+ Assert.assertFalse(revocationCache.isRevoked(RevocationCacheContexts.SINGLE_ACCESS_OR_REFRESH_TOKENS,
+ atClaimsSet.getID()));
}
@Test
- public void testRevokeRefreshToken() {
+ public void testChain_RevokeRefreshToken() throws Exception {
+ setUp(OAuth2TokenRevocationMethod.CHAIN, null);
Assert.assertFalse(revocationCache.isRevoked(RevocationCacheContexts.AUTHORIZATION_CODE, rfClaimsSet.getID()));
+ Assert.assertFalse(revocationCache.isRevoked(RevocationCacheContexts.SINGLE_ACCESS_OR_REFRESH_TOKENS,
+ rfClaimsSet.getID()));
tokenCtx.setTokenClaimsSet(rfClaimsSet.getClaimsSet());
ActionTestingSupport.assertProceedEvent(action.execute(src));
Assert.assertTrue(revocationCache.isRevoked(RevocationCacheContexts.AUTHORIZATION_CODE, rfClaimsSet.getID()));
+ Assert.assertFalse(revocationCache.isRevoked(RevocationCacheContexts.SINGLE_ACCESS_OR_REFRESH_TOKENS,
+ rfClaimsSet.getID()));
+ }
+
+ @Test
+ public void testChain_RevokeAccessTokenViaRootToken() throws Exception {
+ final String rootTokenIdentifier = new SecureRandomIdentifierGenerationStrategy().generateIdentifier();
+ setUp(OAuth2TokenRevocationMethod.CHAIN, rootTokenIdentifier);
+ Assert.assertFalse(revocationCache.isRevoked(RevocationCacheContexts.AUTHORIZATION_CODE, atClaimsSet.getID()));
+ Assert.assertFalse(revocationCache.isRevoked(RevocationCacheContexts.AUTHORIZATION_CODE, rootTokenIdentifier));
+ Assert.assertFalse(revocationCache.isRevoked(RevocationCacheContexts.SINGLE_ACCESS_OR_REFRESH_TOKENS,
+ atClaimsSet.getID()));
+ tokenCtx.setTokenClaimsSet(atClaimsSet.getClaimsSet());
+ ActionTestingSupport.assertProceedEvent(action.execute(src));
+ Assert.assertTrue(revocationCache.isRevoked(RevocationCacheContexts.AUTHORIZATION_CODE, rootTokenIdentifier));
+ Assert.assertFalse(revocationCache.isRevoked(RevocationCacheContexts.AUTHORIZATION_CODE, atClaimsSet.getID()));
+ Assert.assertFalse(revocationCache.isRevoked(RevocationCacheContexts.SINGLE_ACCESS_OR_REFRESH_TOKENS,
+ atClaimsSet.getID()));
+ }
+
+ @Test
+ public void testChain_RevokeRefreshTokenViaRootToken() throws Exception {
+ final String rootTokenIdentifier = new SecureRandomIdentifierGenerationStrategy().generateIdentifier();
+ setUp(OAuth2TokenRevocationMethod.CHAIN, rootTokenIdentifier);
+ Assert.assertFalse(revocationCache.isRevoked(RevocationCacheContexts.AUTHORIZATION_CODE, rfClaimsSet.getID()));
+ Assert.assertFalse(revocationCache.isRevoked(RevocationCacheContexts.AUTHORIZATION_CODE, rootTokenIdentifier));
+ Assert.assertFalse(revocationCache.isRevoked(RevocationCacheContexts.SINGLE_ACCESS_OR_REFRESH_TOKENS,
+ rfClaimsSet.getID()));
+ tokenCtx.setTokenClaimsSet(rfClaimsSet.getClaimsSet());
+ ActionTestingSupport.assertProceedEvent(action.execute(src));
+ Assert.assertTrue(revocationCache.isRevoked(RevocationCacheContexts.AUTHORIZATION_CODE, rootTokenIdentifier));
+ Assert.assertFalse(revocationCache.isRevoked(RevocationCacheContexts.AUTHORIZATION_CODE, rfClaimsSet.getID()));
+ Assert.assertFalse(revocationCache.isRevoked(RevocationCacheContexts.SINGLE_ACCESS_OR_REFRESH_TOKENS,
+ rfClaimsSet.getID()));
+ }
+
+ @Test
+ public void testSingleToken_NoToken() throws Exception {
+ setUp(OAuth2TokenRevocationMethod.TOKEN, null);
+ ActionTestingSupport.assertProceedEvent(action.execute(src));
+ }
+
+ @Test
+ public void testSingleToken_RevokeAccessToken() throws Exception {
+ setUp(OAuth2TokenRevocationMethod.TOKEN, null);
+ Assert.assertFalse(revocationCache.isRevoked(RevocationCacheContexts.SINGLE_ACCESS_OR_REFRESH_TOKENS,
+ atClaimsSet.getID()));
+ Assert.assertFalse(revocationCache.isRevoked(RevocationCacheContexts.AUTHORIZATION_CODE, atClaimsSet.getID()));
+ tokenCtx.setTokenClaimsSet(atClaimsSet.getClaimsSet());
+ ActionTestingSupport.assertProceedEvent(action.execute(src));
+ Assert.assertTrue(revocationCache.isRevoked(RevocationCacheContexts.SINGLE_ACCESS_OR_REFRESH_TOKENS,
+ atClaimsSet.getID()));
+ Assert.assertFalse(revocationCache.isRevoked(RevocationCacheContexts.AUTHORIZATION_CODE, atClaimsSet.getID()));
+ }
+
+ @Test
+ public void testSingleToken_RevokeRefreshToken() throws Exception {
+ setUp(OAuth2TokenRevocationMethod.TOKEN, null);
+ Assert.assertFalse(revocationCache.isRevoked(RevocationCacheContexts.SINGLE_ACCESS_OR_REFRESH_TOKENS,
+ rfClaimsSet.getID()));
+ Assert.assertFalse(revocationCache.isRevoked(RevocationCacheContexts.AUTHORIZATION_CODE, rfClaimsSet.getID()));
+ tokenCtx.setTokenClaimsSet(rfClaimsSet.getClaimsSet());
+ ActionTestingSupport.assertProceedEvent(action.execute(src));
+ Assert.assertTrue(revocationCache.isRevoked(RevocationCacheContexts.SINGLE_ACCESS_OR_REFRESH_TOKENS,
+ rfClaimsSet.getID()));
+ Assert.assertFalse(revocationCache.isRevoked(RevocationCacheContexts.AUTHORIZATION_CODE, rfClaimsSet.getID()));
+ }
+
+ @Test
+ public void testSingleToken_RevokeAccessTokenViaRootToken() throws Exception {
+ final String rootTokenIdentifier = new SecureRandomIdentifierGenerationStrategy().generateIdentifier();
+ setUp(OAuth2TokenRevocationMethod.TOKEN, rootTokenIdentifier);
+ Assert.assertFalse(revocationCache.isRevoked(RevocationCacheContexts.SINGLE_ACCESS_OR_REFRESH_TOKENS,
+ atClaimsSet.getID()));
+ Assert.assertFalse(revocationCache.isRevoked(RevocationCacheContexts.AUTHORIZATION_CODE, atClaimsSet.getID()));
+ Assert.assertFalse(revocationCache.isRevoked(RevocationCacheContexts.AUTHORIZATION_CODE, rootTokenIdentifier));
+ tokenCtx.setTokenClaimsSet(atClaimsSet.getClaimsSet());
+ ActionTestingSupport.assertProceedEvent(action.execute(src));
+ Assert.assertTrue(revocationCache.isRevoked(RevocationCacheContexts.SINGLE_ACCESS_OR_REFRESH_TOKENS,
+ atClaimsSet.getID()));
+ Assert.assertFalse(revocationCache.isRevoked(RevocationCacheContexts.AUTHORIZATION_CODE, atClaimsSet.getID()));
+ Assert.assertFalse(revocationCache.isRevoked(RevocationCacheContexts.AUTHORIZATION_CODE, rootTokenIdentifier));
+ }
+
+ @Test
+ public void testSingleToken_RevokeRefreshTokenViaRootToken() throws Exception {
+ final String rootTokenIdentifier = new SecureRandomIdentifierGenerationStrategy().generateIdentifier();
+ setUp(OAuth2TokenRevocationMethod.TOKEN, rootTokenIdentifier);
+ Assert.assertFalse(revocationCache.isRevoked(RevocationCacheContexts.SINGLE_ACCESS_OR_REFRESH_TOKENS,
+ rfClaimsSet.getID()));
+ Assert.assertFalse(revocationCache.isRevoked(RevocationCacheContexts.AUTHORIZATION_CODE, rfClaimsSet.getID()));
+ Assert.assertFalse(revocationCache.isRevoked(RevocationCacheContexts.AUTHORIZATION_CODE, rootTokenIdentifier));
+ tokenCtx.setTokenClaimsSet(rfClaimsSet.getClaimsSet());
+ ActionTestingSupport.assertProceedEvent(action.execute(src));
+ Assert.assertTrue(revocationCache.isRevoked(RevocationCacheContexts.SINGLE_ACCESS_OR_REFRESH_TOKENS,
+ rfClaimsSet.getID()));
+ Assert.assertFalse(revocationCache.isRevoked(RevocationCacheContexts.AUTHORIZATION_CODE, rfClaimsSet.getID()));
+ Assert.assertFalse(revocationCache.isRevoked(RevocationCacheContexts.AUTHORIZATION_CODE, rootTokenIdentifier));
}
}
\ No newline at end of file
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 3f44e8f6..329f22e8 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
@@ -26,6 +26,8 @@ import net.shibboleth.idp.profile.context.RelyingPartyContext;
import net.shibboleth.idp.profile.testing.ActionTestingSupport;
import net.shibboleth.utilities.java.support.component.ComponentInitializationException;
import net.shibboleth.utilities.java.support.security.DataSealerException;
+import net.shibboleth.utilities.java.support.security.impl.SecureRandomIdentifierGenerationStrategy;
+
import java.net.URI;
import java.net.URISyntaxException;
import java.security.NoSuchAlgorithmException;
@@ -80,14 +82,47 @@ public class SetRefreshTokenToResponseContextTest extends BaseOIDCResponseAction
* @throws DataSealerException
*/
@Test
- public void testSuccess() throws ComponentInitializationException, NoSuchAlgorithmException, URISyntaxException,
+ public void testSuccessViaCode() throws ComponentInitializationException, NoSuchAlgorithmException, URISyntaxException,
+ ParseException, DataSealerException {
+ final String jit = respCtx.getAuthorizationGrantClaimsSet().getID();
+ 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(), jit);
+ }
+
+ @Test
+ public void testSuccessViaRefresh() throws ComponentInitializationException, NoSuchAlgorithmException, URISyntaxException,
ParseException, DataSealerException {
+ final String rootTokenId = new SecureRandomIdentifierGenerationStrategy().generateIdentifier();
+ 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)
+ .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);
}
/**
--
To stop receiving notification emails like this one, please contact
the administrator of this repository.
More information about the commits
mailing list