[java-idp-oidc] branch main updated: JOIDC-92 - Support for refresh token rotation
Henri Mikkonen
henri.mikkonen at iki.fi
Tue Jun 7 11:44:02 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=f973137df55eeb50b04643814bc478185d4b3acd
The following commit(s) were added to refs/heads/main by this push:
new f973137d JOIDC-92 - Support for refresh token rotation
f973137d is described below
commit f973137df55eeb50b04643814bc478185d4b3acd
Author: Henri Mikkonen <henri.mikkonen at iki.fi>
AuthorDate: Tue Jun 7 14:42:21 2022 +0300
JOIDC-92 - Support for refresh token rotation
https://shibboleth.atlassian.net/browse/JOIDC-92
- Always include the full chain (all refresh and access tokens), if revoked refresh token is being used
- Token endpoint revokes a single refresh token when it’s being used, if profile configuration enforces refresh token rotation
---
.../impl/SetRefreshTokenToResponseContext.java | 57 +++++++++++-
.../plugin/oidc/op/profile/impl/ValidateGrant.java | 54 ++++++++++-
.../idp/flows/oidc/token/token-beans.xml | 4 +-
.../plugin/oidc/op/profile/flow/TokenFlowTest.java | 68 +++++++++++++-
.../impl/SetRefreshTokenToResponseContextTest.java | 48 ++++++++++
.../oidc/op/profile/impl/ValidateGrantTest.java | 100 ++++++++++++++++++---
.../src/test/resources/conf/relying-party.xml | 8 ++
7 files changed, 323 insertions(+), 16 deletions(-)
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 18581fe2..b3e151cd 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
@@ -23,12 +23,14 @@ import java.time.Instant;
import java.util.Map;
import java.util.function.BiFunction;
import java.util.function.Function;
+import java.util.function.Predicate;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import org.opensaml.profile.action.EventIds;
import org.opensaml.profile.context.ProfileRequestContext;
+import org.opensaml.storage.RevocationCache;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -36,15 +38,19 @@ import com.nimbusds.jwt.JWTClaimsSet;
import com.nimbusds.openid.connect.sdk.OIDCScopeValue;
import net.shibboleth.idp.plugin.oidc.op.messaging.context.OIDCAuthenticationResponseContext;
+import net.shibboleth.idp.plugin.oidc.op.storage.RevocationCacheContexts;
import net.shibboleth.idp.plugin.oidc.op.token.support.AuthorizeCodeClaimsSet;
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.RefreshTokenClaimsSetManipulationStrategyLookupFunction;
import net.shibboleth.oidc.profile.config.navigate.RefreshTokenLifetimeLookupFunction;
import org.opensaml.profile.action.ActionSupport;
import net.shibboleth.utilities.java.support.annotation.ParameterName;
+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.logic.FunctionSupport;
@@ -68,6 +74,9 @@ public class SetRefreshTokenToResponseContext extends AbstractOIDCResponseAction
/** Data sealer for handling access token. */
@Nonnull private final DataSealer dataSealer;
+ /** Message revocation cache instance to use. */
+ @NonnullAfterInit private RevocationCache revocationCache;
+
/** Strategy used to obtain the refresh token lifetime. */
@Nonnull private Function<ProfileRequestContext,Duration> refreshTokenLifetimeLookupStrategy;
@@ -82,6 +91,9 @@ public class SetRefreshTokenToResponseContext extends AbstractOIDCResponseAction
/** Strategy used to locate the {@link IdentifierGenerationStrategy} to use. */
@Nonnull private Function<ProfileRequestContext,IdentifierGenerationStrategy> idGeneratorLookupStrategy;
+ /** Strategy used to determine whether to revoke refresh tokens once they're used. */
+ @Nonnull private Predicate<ProfileRequestContext> enforceRefreshTokenRotationCondition;
+
/** Authorize Code / Refresh Token the refresh token will be based on. */
@Nullable private TokenClaimsSet tokenClaimsSet;
@@ -102,6 +114,17 @@ public class SetRefreshTokenToResponseContext extends AbstractOIDCResponseAction
tokenClaimsSetManipulationStrategyLookupStrategy =
new RefreshTokenClaimsSetManipulationStrategyLookupFunction();
idGeneratorLookupStrategy = FunctionSupport.constant(new SecureRandomIdentifierGenerationStrategy());
+ enforceRefreshTokenRotationCondition = new EnforceRefreshTokenRotationPredicate();
+ }
+
+ /**
+ * Set the revocation cache instance to use.
+ *
+ * @param cache The revocationCache to set.
+ */
+ public void setRevocationCache(@Nonnull final RevocationCache cache) {
+ ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+ revocationCache = Constraint.isNotNull(cache, "RevocationCache cannot be null");
}
/**
@@ -144,6 +167,27 @@ public class SetRefreshTokenToResponseContext extends AbstractOIDCResponseAction
Constraint.isNotNull(strategy, "Identifier generation strategy cannot be null");
}
+ /**
+ * Set the condition used to determine whether to revoke refresh tokens once they're used.
+ *
+ * @param condition condition to apply
+ */
+ public void setEnforceRefreshTokenRotationCondition(@Nonnull final Predicate<ProfileRequestContext> condition) {
+ ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+
+ enforceRefreshTokenRotationCondition = Constraint.isNotNull(condition, "Condition cannot be null");
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected void doInitialize() throws ComponentInitializationException {
+ super.doInitialize();
+
+ if (revocationCache == null) {
+ throw new ComponentInitializationException("RevocationCache cannot be null");
+ }
+ }
+
/** {@inheritDoc} */
@Override
protected boolean doPreExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
@@ -195,7 +239,7 @@ public class SetRefreshTokenToResponseContext extends AbstractOIDCResponseAction
.setJWTID(idGenerator)
.setRootTokenIdentifier(rootTokenId)
.build();
-
+
if (manipulationStrategy != null) {
log.debug("{} Manipulation strategy has been set, applying it to the claims set {}", getLogPrefix(),
claimsSet.serialize());
@@ -226,6 +270,17 @@ public class SetRefreshTokenToResponseContext extends AbstractOIDCResponseAction
log.error("{} Refresh Token generation failed {}", getLogPrefix(), e.getMessage());
ActionSupport.buildEvent(profileRequestContext, EventIds.UNABLE_TO_ENCRYPT);
}
+
+ if (enforceRefreshTokenRotationCondition.test(profileRequestContext) &&
+ tokenClaimsSet instanceof RefreshTokenClaimsSet) {
+ final String jti = tokenClaimsSet.getID();
+ log.debug("{} Revoking the refresh token {} used for issuing the new one", getLogPrefix(), jti);
+ if (!revocationCache.revoke(RevocationCacheContexts.SINGLE_ACCESS_OR_REFRESH_TOKENS, jti)) {
+ log.error("{} Unable to store revocation into the revocation cache", getLogPrefix());
+ ActionSupport.buildEvent(profileRequestContext, IdPEventIds.INVALID_PROFILE_CONFIG);
+ return;
+ }
+ }
}
}
\ 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 4596acdd..38a83a8b 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
@@ -18,6 +18,7 @@
package net.shibboleth.idp.plugin.oidc.op.profile.impl;
import java.text.ParseException;
+import java.time.Duration;
import java.util.function.Function;
import java.util.function.Predicate;
@@ -45,12 +46,14 @@ 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.RefreshTokenLifetimeLookupFunction;
import net.shibboleth.oidc.profile.core.OidcEventIds;
import net.shibboleth.utilities.java.support.annotation.ParameterName;
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;
import net.shibboleth.utilities.java.support.security.DataSealer;
import net.shibboleth.utilities.java.support.security.DataSealerException;
@@ -89,6 +92,9 @@ public class ValidateGrant extends AbstractOIDCTokenResponseAction {
/** Predicate used to indicate whether refresh tokens are enabled. */
@Nonnull private Predicate<ProfileRequestContext> refreshTokensEnabledPredicate;
+ /** Strategy used to lookup the duration for lifetime of an entry in the token revocation cache. */
+ @Nonnull private Function<ProfileRequestContext, Duration> tokenRevocationLifetimeLookupStrategy;
+
/** The RelyingPartyContext to operate on. */
@Nullable private RelyingPartyContext rpCtx;
@@ -101,6 +107,7 @@ public class ValidateGrant extends AbstractOIDCTokenResponseAction {
dataSealer = Constraint.isNotNull(sealer, "DataSealer cannot be null");
relyingPartyContextLookupStrategy = new ChildContextLookup<>(RelyingPartyContext.class);
refreshTokensEnabledPredicate = new RefreshTokensEnabledPredicate();
+ tokenRevocationLifetimeLookupStrategy = new RefreshTokenLifetimeLookupFunction();
}
/**
@@ -149,6 +156,18 @@ public class ValidateGrant extends AbstractOIDCTokenResponseAction {
revocationCache = Constraint.isNotNull(cache, "RevocationCache cannot be null");
}
+ /**
+ * Set the strategy used to lookup the duration for lifetime of an entry in the token revocation cache.
+ *
+ * @param strategy The strategy to set.
+ */
+ public void setTokenRevocationLifetimeLookupStrategy(
+ @Nonnull final Function<ProfileRequestContext, Duration> strategy) {
+ ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+ tokenRevocationLifetimeLookupStrategy = Constraint.isNotNull(strategy,
+ "The lookup strategy cannot be null");
+ }
+
/** {@inheritDoc} */
@Override
protected void doInitialize() throws ComponentInitializationException {
@@ -222,10 +241,39 @@ public class ValidateGrant extends AbstractOIDCTokenResponseAction {
try {
final RefreshTokenClaimsSet refreshTokenClaimsSet = RefreshTokenClaimsSet
.parse(refreshTokentokenGrant.getRefreshToken().getValue(), dataSealer);
- if (revocationCache.isRevoked(RevocationCacheContexts.AUTHORIZATION_CODE,
- refreshTokenClaimsSet.getID())) {
+ final String rootJti = refreshTokenClaimsSet.getRootTokenIdentifier();
+ final String rootJtiToUse;
+ if (StringSupport.trimOrNull(rootJti) == null) {
+ log.warn("{} No root token identifier returned, using JWT id for checking revocation status",
+ getLogPrefix());
+ rootJtiToUse = refreshTokenClaimsSet.getID();
+ } else {
+ rootJtiToUse = rootJti;
+ }
+ if (revocationCache.isRevoked(RevocationCacheContexts.AUTHORIZATION_CODE, rootJtiToUse)) {
log.error("{} Authz code {} and all derived tokens have been revoked", getLogPrefix(),
- refreshTokenClaimsSet.getID());
+ rootJtiToUse);
+ ActionSupport.buildEvent(profileRequestContext, OidcEventIds.INVALID_GRANT);
+ return;
+ } else if (revocationCache.isRevoked(RevocationCacheContexts.SINGLE_ACCESS_OR_REFRESH_TOKENS,
+ refreshTokenClaimsSet.getID())) {
+ log.error("{} The refresh token {} has been revoked. Revoking the full chain now.",
+ getLogPrefix(), refreshTokenClaimsSet.getID());
+ final Duration lifetime =
+ tokenRevocationLifetimeLookupStrategy.apply(profileRequestContext);
+ if (lifetime == null) {
+ log.error("{} Could not resolve the token revocation lifetime, full chain not revoked",
+ getLogPrefix());
+ ActionSupport.buildEvent(profileRequestContext, IdPEventIds.INVALID_PROFILE_CONFIG);
+ return;
+ }
+ if (!revocationCache.revoke(RevocationCacheContexts.AUTHORIZATION_CODE, rootJtiToUse,
+ lifetime)) {
+ log.error("{} Fatal error, unable to store revocation into the revocation cache",
+ getLogPrefix());
+ ActionSupport.buildEvent(profileRequestContext, IdPEventIds.INVALID_PROFILE_CONFIG);
+ return;
+ }
ActionSupport.buildEvent(profileRequestContext, OidcEventIds.INVALID_GRANT);
return;
}
diff --git a/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/token/token-beans.xml b/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/token/token-beans.xml
index 2a3c1f32..5b042d44 100644
--- a/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/token/token-beans.xml
+++ b/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/token/token-beans.xml
@@ -318,7 +318,9 @@
<bean id="SetRefreshTokenToResponseContext"
class="net.shibboleth.idp.plugin.oidc.op.profile.impl.SetRefreshTokenToResponseContext" scope="prototype"
- c:sealer-ref="#{'%{idp.oidc.tokenSealer:shibboleth.oidc.TokenSealer}'.trim()}">
+ c:sealer-ref="#{'%{idp.oidc.tokenSealer:shibboleth.oidc.TokenSealer}'.trim()}"
+ p:revocationCache-ref="shibboleth.oidc.RevocationCache">
+
<property name="activationCondition">
<bean parent="shibboleth.Conditions.AND">
<constructor-arg>
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 c2b4edd1..8aa9ace5 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
@@ -19,10 +19,12 @@ package net.shibboleth.idp.plugin.oidc.op.profile.flow;
import java.io.IOException;
import java.text.ParseException;
+import java.time.Duration;
import java.time.Instant;
import java.util.HashMap;
import java.util.Map;
+import org.opensaml.storage.RevocationCache;
import org.opensaml.storage.StorageService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
@@ -51,6 +53,7 @@ import com.nimbusds.openid.connect.sdk.claims.ClaimsSet;
import net.minidev.json.JSONObject;
import net.shibboleth.idp.plugin.oidc.op.profile.impl.ValidateGrantTest;
+import net.shibboleth.idp.plugin.oidc.op.storage.RevocationCacheContexts;
import net.shibboleth.idp.plugin.oidc.op.token.support.AccessTokenClaimsSet;
import net.shibboleth.idp.plugin.oidc.op.token.support.RefreshTokenClaimsSet;
import net.shibboleth.idp.plugin.oidc.op.token.support.TokenClaimsSet;
@@ -72,6 +75,7 @@ public class TokenFlowTest extends AbstractOidcClientAuthenticationFlowTest {
String clientIdPkcePlainUnforcedPublic = "mockPublicClientIdPKCEPlainUnforced";
String clientIdPkceS256Public = "mockPublicClientIdPKCES256";
String clientIdCustomTokens = "mockClientIdCustomTokens";
+ String clientIdRefreshTokenRotation = "mockClientIdRefreshTokenRotation";
String codeVerifier = "9234567812345678123456781234567812345678123456781234567812345678";
Scope scope = Scope.parse("openid profile email offline_access");
@@ -80,6 +84,10 @@ public class TokenFlowTest extends AbstractOidcClientAuthenticationFlowTest {
@Qualifier("shibboleth.StorageService")
StorageService storageService;
+ @Autowired
+ @Qualifier("shibboleth.oidc.RevocationCache")
+ RevocationCache revocationCache;
+
public TokenFlowTest() {
super(FLOW_ID);
}
@@ -90,6 +98,7 @@ public class TokenFlowTest extends AbstractOidcClientAuthenticationFlowTest {
removeMetadata(storageService, clientIdPkcePlain);
removeMetadata(storageService, clientIdPkceS256);
removeMetadata(storageService, clientIdCustomTokens);
+ removeMetadata(storageService, clientIdRefreshTokenRotation);
}
@Test
@@ -354,6 +363,18 @@ public class TokenFlowTest extends AbstractOidcClientAuthenticationFlowTest {
return new RefreshToken(getDataSealer().wrap(json, Instant.now().plusSeconds(30))).getValue();
}
+ protected String buildRefreshToken(final String clientId, final String id, final String rootId,
+ final String... consentedClaims) throws Exception {
+ final TokenClaimsSet acClaims = ValidateGrantTest.buildTokenClaimsSet(clientId, "https://op.example.org", "jdoe", "mock", redirectUri,
+ null, null, null, null, scope.toString());
+ final RefreshTokenClaimsSet rtClaims = new RefreshTokenClaimsSet.Builder(acClaims, Instant.now(),
+ Instant.now().plus(Duration.ofHours(1)))
+ .setRootTokenIdentifier(rootId)
+ .setJWTID(id)
+ .build();
+ return new RefreshToken(rtClaims.serialize(new ValidateGrantTest().getDataSealer())).getValue();
+ }
+
@Test
public void testValidSecretJWT() throws Exception {
final ClientSecretJWT clientAuth = buildSecretJwtAuth(clientSecret);
@@ -570,7 +591,52 @@ public class TokenFlowTest extends AbstractOidcClientAuthenticationFlowTest {
Assert.assertNotNull(response.getOIDCTokens().getIDToken());
validateConsentFromAccessToken(response, true);
}
-
+
+ @Test
+ public void testValidRefreshTokenGrant() throws Exception {
+ final String id = idGenerator.generateIdentifier();
+ final String rootId = idGenerator.generateIdentifier();
+ initializeGrantAndRequest(clientId, createRequestParameters(redirectUri, "refresh_token",
+ buildRefreshToken(clientId, id, rootId), clientId));
+ final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+ final OIDCTokenResponse response = parseSuccessResponse(result, OIDCTokenResponse.class);
+ final AccessToken accessToken = response.getTokens().getAccessToken();
+ Assert.assertNotNull(accessToken);
+ Assert.assertNotNull(response.getOIDCTokens().getIDToken());
+ validateConsentFromAccessToken(response, false);
+ Assert.assertFalse(revocationCache.isRevoked(RevocationCacheContexts.SINGLE_ACCESS_OR_REFRESH_TOKENS, id));
+ Assert.assertFalse(revocationCache.isRevoked(RevocationCacheContexts.AUTHORIZATION_CODE, rootId));
+ }
+
+ @Test
+ public void testValidRefreshTokenGrantRefreshTokenRotation() throws Exception {
+ final String id = idGenerator.generateIdentifier();
+ final String rootId = idGenerator.generateIdentifier();
+ initializeGrantAndRequest(clientIdRefreshTokenRotation, createRequestParameters(redirectUri, "refresh_token",
+ buildRefreshToken(clientIdRefreshTokenRotation, id, rootId), clientIdRefreshTokenRotation));
+ final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+ final OIDCTokenResponse response = parseSuccessResponse(result, OIDCTokenResponse.class);
+ final AccessToken accessToken = response.getTokens().getAccessToken();
+ Assert.assertNotNull(accessToken);
+ Assert.assertNotNull(response.getOIDCTokens().getIDToken());
+ validateConsentFromAccessToken(response, false);
+ Assert.assertTrue(revocationCache.isRevoked(RevocationCacheContexts.SINGLE_ACCESS_OR_REFRESH_TOKENS, id));
+ Assert.assertFalse(revocationCache.isRevoked(RevocationCacheContexts.AUTHORIZATION_CODE, rootId));
+ }
+
+ @Test
+ public void testRevokedRefreshTokenGrantRefreshTokenRotation() throws Exception {
+ final String id = idGenerator.generateIdentifier();
+ final String rootId = idGenerator.generateIdentifier();
+ Assert.assertTrue(revocationCache.revoke(RevocationCacheContexts.SINGLE_ACCESS_OR_REFRESH_TOKENS, id));
+ initializeGrantAndRequest(clientIdRefreshTokenRotation, createRequestParameters(redirectUri, "refresh_token",
+ buildRefreshToken(clientIdRefreshTokenRotation, id, rootId), clientIdRefreshTokenRotation));
+ final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+ assertErrorCode(result, OAuth2Error.INVALID_GRANT_CODE);
+ Assert.assertTrue(revocationCache.isRevoked(RevocationCacheContexts.SINGLE_ACCESS_OR_REFRESH_TOKENS, id));
+ Assert.assertTrue(revocationCache.isRevoked(RevocationCacheContexts.AUTHORIZATION_CODE, rootId));
+ }
+
private AccessTokenClaimsSet unwrapAccessToken(final OIDCTokenResponse tokenResponse) {
final AccessToken accessToken = tokenResponse.getTokens().getAccessToken();
Assert.assertNotNull(accessToken);
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 329f22e8..fe5468f2 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
@@ -17,6 +17,7 @@
package net.shibboleth.idp.plugin.oidc.op.profile.impl;
+import net.shibboleth.idp.plugin.oidc.op.storage.RevocationCacheContexts;
import net.shibboleth.idp.plugin.oidc.op.token.support.AccessTokenClaimsSet;
import net.shibboleth.idp.plugin.oidc.op.token.support.AuthorizeCodeClaimsSet;
import net.shibboleth.idp.plugin.oidc.op.token.support.RefreshTokenClaimsSet;
@@ -35,6 +36,8 @@ import java.text.ParseException;
import java.time.Instant;
import org.opensaml.profile.action.EventIds;
+import org.opensaml.storage.RevocationCache;
+import org.opensaml.storage.impl.MemoryStorageService;
import org.springframework.webflow.execution.Event;
import org.testng.Assert;
import org.testng.annotations.BeforeMethod;
@@ -49,6 +52,10 @@ public class SetRefreshTokenToResponseContextTest extends BaseOIDCResponseAction
private SetRefreshTokenToResponseContext action;
+ RevocationCache revocationCache;
+
+ private boolean enforceRotation;
+
@BeforeMethod
private void init() throws ComponentInitializationException, NoSuchAlgorithmException, URISyntaxException {
final Scope scope = new Scope();
@@ -69,6 +76,14 @@ public class SetRefreshTokenToResponseContextTest extends BaseOIDCResponseAction
.build();
respCtx.setAuthorizationGrantClaimsSet(claims);
action = new SetRefreshTokenToResponseContext(getDataSealer());
+ final MemoryStorageService storageService = new MemoryStorageService();
+ storageService.setId("id");
+ storageService.initialize();
+ revocationCache = new RevocationCache();
+ revocationCache.setStorage(storageService);
+ action.setRevocationCache(revocationCache);
+ enforceRotation = false;
+ action.setEnforceRefreshTokenRotationCondition(prc -> enforceRotation);
action.initialize();
}
@@ -123,6 +138,39 @@ public class SetRefreshTokenToResponseContextTest extends BaseOIDCResponseAction
Assert.assertNotNull(rt);
Assert.assertNotEquals(rt.getID(), jit);
Assert.assertEquals(rt.getRootTokenIdentifier(), rootTokenId);
+ Assert.assertFalse(revocationCache.isRevoked(RevocationCacheContexts.SINGLE_ACCESS_OR_REFRESH_TOKENS, jit));
+ }
+
+ @Test
+ public void testSuccessViaRefreshRotationEnforced() 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);
+ enforceRotation = true;
+ 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.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 8574ab6e..6fd39c12 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
@@ -19,9 +19,11 @@ package net.shibboleth.idp.plugin.oidc.op.profile.impl;
import net.minidev.json.JSONObject;
import net.shibboleth.idp.plugin.oidc.op.messaging.context.OIDCAuthenticationResponseContext;
+import net.shibboleth.idp.plugin.oidc.op.storage.RevocationCacheContexts;
import net.shibboleth.idp.plugin.oidc.op.token.support.AuthorizeCodeClaimsSet;
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.idp.profile.testing.ActionTestingSupport;
import net.shibboleth.oidc.profile.core.OidcEventIds;
import net.shibboleth.utilities.java.support.component.ComponentInitializationException;
@@ -30,11 +32,16 @@ import net.shibboleth.utilities.java.support.security.impl.SecureRandomIdentifie
import java.net.URI;
import java.security.NoSuchAlgorithmException;
+import java.time.Duration;
import java.time.Instant;
+import java.util.function.Function;
+import org.opensaml.profile.context.ProfileRequestContext;
import org.opensaml.storage.ReplayCache;
+import org.opensaml.storage.RevocationCache;
import org.opensaml.storage.impl.MemoryStorageService;
import org.testng.Assert;
+import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;
import com.google.common.base.Predicates;
@@ -65,13 +72,30 @@ public class ValidateGrantTest extends BaseOIDCResponseActionTest {
RefreshTokenGrant rfGrant;
URI callback;
+
+ MemoryStorageService storageService;
+
+ String rootTokenId;
+ @BeforeMethod
+ protected void setupStorage() throws ComponentInitializationException {
+ storageService = new MemoryStorageService();
+ storageService.setId("id");
+ storageService.initialize();
+ }
+
private void init() throws Exception {
init(true);
}
-
+
private void init(boolean refreshTokensEnabled) throws Exception {
+ init(refreshTokensEnabled, new MockRevocationCache(false, true), null);
+ }
+
+ private void init(boolean refreshTokensEnabled, final RevocationCache revocationCache,
+ final Function<ProfileRequestContext, Duration> revocationLifetimeLookup) throws Exception {
final Instant now = Instant.now();
+ rootTokenId = "mockId" + now.toEpochMilli();
acClaims = new AuthorizeCodeClaimsSet.Builder()
.setJWTID(idGenerator)
.setClientID(new ClientID(clientId))
@@ -85,7 +109,9 @@ public class ValidateGrantTest extends BaseOIDCResponseActionTest {
.setScope(new Scope())
.build();
- rfClaims = new RefreshTokenClaimsSet.Builder(acClaims, now, now.plusSeconds(100)).build();
+ rfClaims = new RefreshTokenClaimsSet.Builder(acClaims, now, now.plusSeconds(100))
+ .setRootTokenIdentifier(rootTokenId)
+ .build();
final AuthorizationCode code = new AuthorizationCode(acClaims.serialize(getDataSealer()));
final RefreshToken rfToken = new RefreshToken(rfClaims.serialize(getDataSealer()));
callback = new URI("https://client.com/callback");
@@ -95,11 +121,11 @@ public class ValidateGrantTest extends BaseOIDCResponseActionTest {
final TokenRequest req = new TokenRequest(callback, new ClientID(clientId), codeGrant);
profileRequestCtx.getInboundMessageContext().setMessage(req);
action = new ValidateGrant(getDataSealer());
- action.setRevocationCache(new MockRevocationCache(false, true));
+ if (revocationLifetimeLookup != null) {
+ action.setTokenRevocationLifetimeLookupStrategy(revocationLifetimeLookup);
+ }
+ action.setRevocationCache(revocationCache);
final ReplayCache replayCache = new ReplayCache();
- final MemoryStorageService storageService = new MemoryStorageService();
- storageService.setId("id");
- storageService.initialize();
replayCache.setStorage(storageService);
action.setReplayCache(replayCache);
if (refreshTokensEnabled) {
@@ -131,8 +157,16 @@ public class ValidateGrantTest extends BaseOIDCResponseActionTest {
final String userPrincipal, final String sub, final String callbackUrl, final String codeChallenge,
final JSONObject deliveryClaims, final JSONObject deliveryClaimsIDToken,
final JSONObject deliveryClaimsUserInfo, final String scope) throws Exception {
+ final TokenClaimsSet acClaims = buildTokenClaimsSet(clientId, issuer, userPrincipal, sub, callbackUrl,
+ codeChallenge, deliveryClaims, deliveryClaimsIDToken, deliveryClaimsUserInfo, scope);
+ return new AuthorizationCode(acClaims.serialize(new ValidateGrantTest().getDataSealer()));
+ }
+
+ static public TokenClaimsSet buildTokenClaimsSet(final String clientId, final String issuer,
+ final String userPrincipal, final String sub, final String callbackUrl, final String codeChallenge,
+ final JSONObject deliveryClaims, final JSONObject deliveryClaimsIDToken,
+ final JSONObject deliveryClaimsUserInfo, final String scope) throws Exception {
final Instant now = Instant.now();
- final ValidateGrantTest test = new ValidateGrantTest();
final AuthorizeCodeClaimsSet.Builder builder = new AuthorizeCodeClaimsSet.Builder();
builder.setJWTID(new SecureRandomIdentifierGenerationStrategy())
.setClientID(new ClientID(clientId))
@@ -157,10 +191,9 @@ public class ValidateGrantTest extends BaseOIDCResponseActionTest {
if (deliveryClaimsUserInfo != null) {
builder.setDlClaimsUI(new DeliveryClaimsSet(deliveryClaimsUserInfo));
}
- final TokenClaimsSet acClaims = builder.build();
- return new AuthorizationCode(acClaims.serialize(test.getDataSealer()));
+ return builder.build();
}
-
+
@Test
public void testAuthorizeCodeSuccess() throws Exception {
init();
@@ -211,6 +244,53 @@ public class ValidateGrantTest extends BaseOIDCResponseActionTest {
Assert.assertNotNull(arc.getAuthorizationGrantClaimsSet());
}
+ @Test
+ public void testRefreshTokenAuthorizationGrantRevoked() throws Exception {
+ final RevocationCache revocationCache = new RevocationCache();
+ revocationCache.setStorage(storageService);
+ init(true, revocationCache, null);
+ Assert.assertTrue(revocationCache.revoke(RevocationCacheContexts.AUTHORIZATION_CODE,
+ rfClaims.getRootTokenIdentifier()));
+ 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 testRefreshTokenRevokedShouldRevokeAuthorizationCode() throws Exception {
+ final RevocationCache revocationCache = new RevocationCache();
+ revocationCache.setStorage(storageService);
+ init(true, revocationCache, null);
+ Assert.assertTrue(revocationCache.revoke(RevocationCacheContexts.SINGLE_ACCESS_OR_REFRESH_TOKENS,
+ rfClaims.getID()));
+ 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());
+ Assert.assertTrue(revocationCache.isRevoked(RevocationCacheContexts.AUTHORIZATION_CODE,
+ rfClaims.getRootTokenIdentifier()));
+ }
+
+ @Test
+ public void testRefreshTokenRevokedLifetimeRequired() throws Exception {
+ final RevocationCache revocationCache = new RevocationCache();
+ revocationCache.setStorage(storageService);
+ init(true, revocationCache, prc -> null);
+ Assert.assertTrue(revocationCache.revoke(RevocationCacheContexts.SINGLE_ACCESS_OR_REFRESH_TOKENS,
+ rfClaims.getID()));
+ final TokenRequest req = new TokenRequest(callback, new ClientID(clientId), rfGrant);
+ profileRequestCtx.getInboundMessageContext().setMessage(req);
+ ActionTestingSupport.assertEvent(action.execute(requestCtx), IdPEventIds.INVALID_PROFILE_CONFIG);
+ final OIDCAuthenticationResponseContext arc =
+ profileRequestCtx.getOutboundMessageContext().getSubcontext(OIDCAuthenticationResponseContext.class);
+ Assert.assertNull(arc.getAuthorizationGrantClaimsSet());
+ }
+
@Test
public void testMixGrant() throws Exception {
init();
diff --git a/idp-oidc-extension-impl/src/test/resources/conf/relying-party.xml b/idp-oidc-extension-impl/src/test/resources/conf/relying-party.xml
index e6da1687..7b4a256c 100644
--- a/idp-oidc-extension-impl/src/test/resources/conf/relying-party.xml
+++ b/idp-oidc-extension-impl/src/test/resources/conf/relying-party.xml
@@ -58,6 +58,14 @@
</bean>
<util:list id="shibboleth.RelyingPartyOverrides">
+ <bean parent="RelyingPartyByName" c:relyingPartyIds="mockClientIdRefreshTokenRotation">
+ <property name="profileConfigurations">
+ <list>
+ <ref bean="OIDC.SSO.MDDriven" />
+ <bean parent="OAUTH2.Token.MDDriven" p:enforceRefreshTokenRotation="true "/>
+ </list>
+ </property>
+ </bean>
<bean parent="RelyingPartyByName" c:relyingPartyIds="mockClientIdCustomTokens">
<property name="profileConfigurations">
<list>
--
To stop receiving notification emails like this one, please contact
the administrator of this repository.
More information about the commits
mailing list