[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