[java-idp-oidc] branch main updated: JOIDC-90 - Revocation of individual tokens
Henri Mikkonen
henri.mikkonen at iki.fi
Thu Jun 16 13:45:18 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=a95e0dba5e90f52917bdb66a08675f1d291567ea
The following commit(s) were added to refs/heads/main by this push:
new a95e0dba JOIDC-90 - Revocation of individual tokens
a95e0dba is described below
commit a95e0dba5e90f52917bdb66a08675f1d291567ea
Author: Henri Mikkonen <henri.mikkonen at iki.fi>
AuthorDate: Thu Jun 16 16:43:23 2022 +0300
JOIDC-90 - Revocation of individual tokens
https://shibboleth.atlassian.net/browse/JOIDC-90
New RootTokenIdRevocationValidator verifies if the token chain has been revoked.
The previously existing JWTIDRevocationClaimsValidator now checks the individual
token revocation context. Improved flow tests for token, userinfo and introspection
endpoints to verify that revocation works as expected.
---
.../impl/RootTokenIdRevocationValidator.java | 104 +++++++++++++++++++++
.../op/security/jwt/claims/impl/package-info.java | 19 ++++
.../idp/service/relying-party/postconfig.xml | 7 ++
.../op/profile/flow/IntrospectionFlowTest.java | 64 ++++++++++++-
.../plugin/oidc/op/profile/flow/TokenFlowTest.java | 23 +++++
.../plugin/oidc/op/profile/flow/UserInfoTest.java | 45 +++++++++
6 files changed, 261 insertions(+), 1 deletion(-)
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/security/jwt/claims/impl/RootTokenIdRevocationValidator.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/security/jwt/claims/impl/RootTokenIdRevocationValidator.java
new file mode 100644
index 00000000..7d53e3b4
--- /dev/null
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/security/jwt/claims/impl/RootTokenIdRevocationValidator.java
@@ -0,0 +1,104 @@
+/*
+ * Licensed to the University Corporation for Advanced Internet Development,
+ * Inc. (UCAID) under one or more contributor license agreements. See the
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID licenses this file to You under the Apache
+ * License, Version 2.0 (the "License"); you may not use this file except in
+ * compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.idp.plugin.oidc.op.security.jwt.claims.impl;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.opensaml.storage.RevocationCache;
+
+import com.nimbusds.jwt.JWTClaimsSet;
+
+import net.shibboleth.idp.plugin.oidc.op.token.support.TokenClaimsSet;
+import net.shibboleth.oidc.jwt.claims.AbstractClaimsValidator;
+import net.shibboleth.oidc.jwt.claims.JWTValidationException;
+import net.shibboleth.utilities.java.support.annotation.constraint.NonnullAfterInit;
+import net.shibboleth.utilities.java.support.annotation.constraint.NotEmpty;
+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;
+
+/**
+ * Verifies the root identifier ({@link TokenClaimsSet#KEY_ROOT_JTI} from the JWT against revocation via configurable
+ * {@link RevocationCache}. If the root identifier is not found, JWT id is used.
+ */
+public class RootTokenIdRevocationValidator extends AbstractClaimsValidator {
+
+ /** Message revocation cache instance to use. */
+ @NonnullAfterInit private RevocationCache revocationCache;
+
+ /** Context in revocation cache. */
+ @NonnullAfterInit @NotEmpty private String context;
+
+ /**
+ * Set the revocation cache instance to use.
+ *
+ * @param cache revocation cache to set
+ */
+ public void setRevocationCache(@Nonnull final RevocationCache cache) {
+ ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+ revocationCache = Constraint.isNotNull(cache, "RevocationCache cannot be null");
+ }
+
+ /**
+ * Set the revocation cache context that partitions entries.
+ *
+ * @param ctx context value
+ */
+ public void setContext(@Nonnull @NotEmpty final String ctx) {
+ ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+ context = Constraint.isNotNull(StringSupport.trimOrNull(ctx), "Context cannot be null or empty");
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected void doInitialize() throws ComponentInitializationException {
+ super.doInitialize();
+
+ if (revocationCache == null) {
+ throw new ComponentInitializationException("RevocationCache cannot be null");
+ } else if (context == null) {
+ throw new ComponentInitializationException("Context cannot be null");
+ }
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected void doValidate(@Nonnull final JWTClaimsSet claims,
+ @Nullable final ProfileRequestContext profileRequestContext) throws JWTValidationException {
+
+ final String rootJti = (String) claims.getClaim(TokenClaimsSet.KEY_ROOT_JTI);
+ final String rootJtiToUse;
+ if (StringSupport.trimOrNull(rootJti) == null) {
+ rootJtiToUse = claims.getJWTID();
+ } else {
+ rootJtiToUse = rootJti;
+ }
+
+ if (StringSupport.trimOrNull(rootJtiToUse) == null) {
+ throw new JWTValidationException("Claims set is missing required JWT identifier claim");
+ }
+
+ if (revocationCache.isRevoked(context, rootJtiToUse)) {
+ throw new JWTValidationException("Claims set with root ID '" + rootJtiToUse + "' has been revoked");
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/security/jwt/claims/impl/package-info.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/security/jwt/claims/impl/package-info.java
new file mode 100644
index 00000000..2151d9f3
--- /dev/null
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/security/jwt/claims/impl/package-info.java
@@ -0,0 +1,19 @@
+/*
+ * 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.
+ */
+
+/** Validation functions for JWT claims. */
+package net.shibboleth.idp.plugin.oidc.op.security.jwt.claims.impl;
\ No newline at end of file
diff --git a/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/service/relying-party/postconfig.xml b/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/service/relying-party/postconfig.xml
index 99f7170d..21598eaa 100644
--- a/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/service/relying-party/postconfig.xml
+++ b/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/service/relying-party/postconfig.xml
@@ -578,6 +578,11 @@
<bean id="JWTIDRevocationClaimsValidator"
class="net.shibboleth.oidc.security.jwt.claims.impl.JWTIdentifierRevocationValidator"
p:revocationCache-ref="shibboleth.oidc.RevocationCache"
+ p:context="#{T(net.shibboleth.idp.plugin.oidc.op.storage.RevocationCacheContexts).SINGLE_ACCESS_OR_REFRESH_TOKENS}" />
+
+ <bean id="RootJWTIDRevocationClaimsValidator"
+ class="net.shibboleth.idp.plugin.oidc.op.security.jwt.claims.impl.RootTokenIdRevocationValidator"
+ p:revocationCache-ref="shibboleth.oidc.RevocationCache"
p:context="#{T(net.shibboleth.idp.plugin.oidc.op.storage.RevocationCacheContexts).AUTHORIZATION_CODE}" />
<util:list id="IntrospectionClaimsValidators" value-type="net.shibboleth.oidc.jwt.claims.ClaimsValidator">
@@ -596,6 +601,7 @@
</property>
</bean>
<ref bean="JWTIDRevocationClaimsValidator" />
+ <ref bean="RootJWTIDRevocationClaimsValidator" />
</util:list>
<util:list id="RevocationClaimsValidators" value-type="net.shibboleth.oidc.jwt.claims.ClaimsValidator">
@@ -639,6 +645,7 @@
</bean>
<ref bean="OPInAudienceClaimsValidator" />
<ref bean="JWTIDRevocationClaimsValidator" />
+ <ref bean="RootJWTIDRevocationClaimsValidator" />
</util:list>
<!--
diff --git a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/IntrospectionFlowTest.java b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/IntrospectionFlowTest.java
index d9cfb6ac..83d3848c 100644
--- a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/IntrospectionFlowTest.java
+++ b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/IntrospectionFlowTest.java
@@ -26,6 +26,7 @@ import java.util.HashMap;
import java.util.List;
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;
@@ -45,6 +46,7 @@ import com.nimbusds.oauth2.sdk.auth.ClientAuthenticationMethod;
import com.nimbusds.oauth2.sdk.id.Audience;
import com.nimbusds.oauth2.sdk.id.ClientID;
+import net.shibboleth.idp.plugin.oidc.op.storage.RevocationCacheContexts;
import net.shibboleth.idp.plugin.oidc.op.token.support.AccessTokenClaimsSet;
import net.shibboleth.oidc.security.credential.JWKCredential;
import net.shibboleth.utilities.java.support.collection.Pair;
@@ -69,7 +71,11 @@ public class IntrospectionFlowTest extends AbstractOidcClientAuthenticationFlowT
@Autowired
@Qualifier("shibboleth.StorageService")
private StorageService storageService;
-
+
+ @Autowired
+ @Qualifier("shibboleth.oidc.RevocationCache")
+ private RevocationCache revocationCache;
+
public IntrospectionFlowTest() {
super(FLOW_ID);
}
@@ -164,6 +170,62 @@ public class IntrospectionFlowTest extends AbstractOidcClientAuthenticationFlowT
Assert.assertNull(resp.getAudience());
}
+ @Test
+ public void testRevokedSingleToken() throws IOException, NoSuchAlgorithmException, URISyntaxException, DataSealerException,
+ ComponentInitializationException {
+ final String rootId = idGenerator.generateIdentifier();
+ final String jti = idGenerator.generateIdentifier();
+ revocationCache.revoke(RevocationCacheContexts.SINGLE_ACCESS_OR_REFRESH_TOKENS, jti);
+ storeMetadata(storageService, clientId, clientSecret, scope);
+ setBasicAuth(clientId, clientSecret);
+ setHttpFormRequest("POST", Map.of(
+ "token",
+ buildToken(clientId, "sub", Scope.parse("openid"), null, jti, rootId).toJSONObject().getAsString("access_token"),
+ "token_type",
+ "access_token"));
+ final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+ final TokenIntrospectionSuccessResponse resp =
+ parseSuccessResponse(result, TokenIntrospectionSuccessResponse.class);
+ Assert.assertFalse(resp.isActive());
+ }
+
+ @Test
+ public void testRevokedChain() throws IOException, NoSuchAlgorithmException, URISyntaxException, DataSealerException,
+ ComponentInitializationException {
+ final String rootId = idGenerator.generateIdentifier();
+ final String jti = idGenerator.generateIdentifier();
+ revocationCache.revoke(RevocationCacheContexts.AUTHORIZATION_CODE, rootId);
+ storeMetadata(storageService, clientId, clientSecret, scope);
+ setBasicAuth(clientId, clientSecret);
+ setHttpFormRequest("POST", Map.of(
+ "token",
+ buildToken(clientId, "sub", Scope.parse("openid"), null, jti, rootId).toJSONObject().getAsString("access_token"),
+ "token_type",
+ "access_token"));
+ final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+ final TokenIntrospectionSuccessResponse resp =
+ parseSuccessResponse(result, TokenIntrospectionSuccessResponse.class);
+ Assert.assertFalse(resp.isActive());
+ }
+
+ @Test
+ public void testRevokedChainViaJti() throws IOException, NoSuchAlgorithmException, URISyntaxException, DataSealerException,
+ ComponentInitializationException {
+ final String jti = idGenerator.generateIdentifier();
+ revocationCache.revoke(RevocationCacheContexts.AUTHORIZATION_CODE, jti);
+ storeMetadata(storageService, clientId, clientSecret, scope);
+ setBasicAuth(clientId, clientSecret);
+ setHttpFormRequest("POST", Map.of(
+ "token",
+ buildToken(clientId, "sub", Scope.parse("openid"), null, jti, null).toJSONObject().getAsString("access_token"),
+ "token_type",
+ "access_token"));
+ final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+ final TokenIntrospectionSuccessResponse resp =
+ parseSuccessResponse(result, TokenIntrospectionSuccessResponse.class);
+ Assert.assertFalse(resp.isActive());
+ }
+
@Test
public void testSuccessWithSamlMetadata() throws NoSuchAlgorithmException, URISyntaxException, DataSealerException,
ComponentInitializationException {
diff --git a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/TokenFlowTest.java b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/TokenFlowTest.java
index 12d7ecc4..42d73448 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
@@ -643,6 +643,29 @@ public class TokenFlowTest extends AbstractOidcClientAuthenticationFlowTest {
Assert.assertTrue(revocationCache.isRevoked(RevocationCacheContexts.AUTHORIZATION_CODE, rootId));
}
+ @Test
+ public void testRevokedChainInRefreshTokenGrant() throws Exception {
+ final String id = idGenerator.generateIdentifier();
+ final String rootId = idGenerator.generateIdentifier();
+ Assert.assertTrue(revocationCache.revoke(RevocationCacheContexts.AUTHORIZATION_CODE, rootId));
+ 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.AUTHORIZATION_CODE, rootId));
+ }
+
+ @Test
+ public void testRevokedChainViaJtiInRefreshTokenGrant() throws Exception {
+ final String id = idGenerator.generateIdentifier();
+ Assert.assertTrue(revocationCache.revoke(RevocationCacheContexts.AUTHORIZATION_CODE, id));
+ initializeGrantAndRequest(clientIdRefreshTokenRotation, createRequestParameters(redirectUri, "refresh_token",
+ buildRefreshToken(clientIdRefreshTokenRotation, id, null), clientIdRefreshTokenRotation));
+ final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+ assertErrorCode(result, OAuth2Error.INVALID_GRANT_CODE);
+ Assert.assertTrue(revocationCache.isRevoked(RevocationCacheContexts.AUTHORIZATION_CODE, id));
+ }
+
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/flow/UserInfoTest.java b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/UserInfoTest.java
index 2b1d4155..ea4cd4fe 100644
--- a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/UserInfoTest.java
+++ b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/UserInfoTest.java
@@ -22,6 +22,7 @@ import java.net.URISyntaxException;
import java.security.NoSuchAlgorithmException;
import java.text.ParseException;
+import org.opensaml.storage.RevocationCache;
import org.opensaml.storage.StorageService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
@@ -43,6 +44,7 @@ import com.nimbusds.openid.connect.sdk.UserInfoSuccessResponse;
import com.nimbusds.openid.connect.sdk.claims.ClaimsSet;
import com.nimbusds.openid.connect.sdk.claims.UserInfo;
+import net.shibboleth.idp.plugin.oidc.op.storage.RevocationCacheContexts;
import net.shibboleth.utilities.java.support.component.ComponentInitializationException;
import net.shibboleth.utilities.java.support.security.DataSealerException;
@@ -63,6 +65,10 @@ public class UserInfoTest extends AbstractOidcApiFlowTest {
@Qualifier("shibboleth.StorageService")
StorageService storageService;
+ @Autowired
+ @Qualifier("shibboleth.oidc.RevocationCache")
+ private RevocationCache revocationCache;
+
public UserInfoTest() {
super(FLOW_ID);
}
@@ -257,4 +263,43 @@ public class UserInfoTest extends AbstractOidcApiFlowTest {
Assert.assertEquals(claimsSet.getClaim("email"), "jdoe at example.org");
Assert.assertEquals(claimsSet.getClaim("iss"), "https://op.example.org");
}
+
+ @Test
+ public void testRevokedSingleToken() throws URISyntaxException, NoSuchAlgorithmException, DataSealerException,
+ ComponentInitializationException, IOException {
+ final String rootId = idGenerator.generateIdentifier();
+ final String jti = idGenerator.generateIdentifier();
+ revocationCache.revoke(RevocationCacheContexts.SINGLE_ACCESS_OR_REFRESH_TOKENS, jti);
+ final BearerAccessToken token = buildToken(clientId, subject, new Scope("openid"), null, jti, rootId);
+ storeMetadata(storageService, clientId, "mockSecret", scope);
+ request.addHeader("Authorization", token.toAuthorizationHeader());
+ final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+ assertErrorCode(result, OAuth2Error.INVALID_GRANT_CODE);
+ }
+
+ @Test
+ public void testRevokedChain() throws URISyntaxException, NoSuchAlgorithmException, DataSealerException,
+ ComponentInitializationException, IOException {
+ final String rootId = idGenerator.generateIdentifier();
+ final String jti = idGenerator.generateIdentifier();
+ revocationCache.revoke(RevocationCacheContexts.AUTHORIZATION_CODE, rootId);
+ final BearerAccessToken token = buildToken(clientId, subject, new Scope("openid"), null, jti, rootId);
+ storeMetadata(storageService, clientId, "mockSecret", scope);
+ request.addHeader("Authorization", token.toAuthorizationHeader());
+ final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+ assertErrorCode(result, OAuth2Error.INVALID_GRANT_CODE);
+ }
+
+ @Test
+ public void testRevokedChainViaJti() throws URISyntaxException, NoSuchAlgorithmException, DataSealerException,
+ ComponentInitializationException, IOException {
+ final String jti = idGenerator.generateIdentifier();
+ revocationCache.revoke(RevocationCacheContexts.AUTHORIZATION_CODE, jti);
+ final BearerAccessToken token = buildToken(clientId, subject, new Scope("openid"), null, jti, null);
+ storeMetadata(storageService, clientId, "mockSecret", scope);
+ request.addHeader("Authorization", token.toAuthorizationHeader());
+ final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+ assertErrorCode(result, OAuth2Error.INVALID_GRANT_CODE);
+ }
+
}
--
To stop receiving notification emails like this one, please contact
the administrator of this repository.
More information about the commits
mailing list