[java-idp-oidc] branch main updated: JOIDC-11 - Support for client_credentials grant

Scott Cantor cantor.2 at osu.edu
Wed Jan 26 22:44:08 UTC 2022


This is an automated email from the git hooks/post-receive script.

scantor 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=9b9c15305ced5f92d221c479e1b6d68448d2c48a

The following commit(s) were added to refs/heads/main by this push:
     new 9b9c1530 JOIDC-11 - Support for client_credentials grant
9b9c1530 is described below

commit 9b9c15305ced5f92d221c479e1b6d68448d2c48a
Author: Scott Cantor <cantor.2 at osu.edu>
AuthorDate: Wed Jan 26 17:44:05 2022 -0500

    JOIDC-11 - Support for client_credentials grant
    
    https://shibboleth.atlassian.net/browse/JOIDC-11
    
    Generalize introspection flow for proper claims validation.
    Proper support for extended response content.
    Preliminary JWT support, untested.
    Migrate RevocationCache out of OP plugin.
    Move RevocationCache bean up to global view.
---
 .../OAuth2TokenIntrospectionResponseContext.java   |  63 +++++
 .../op/oauth2/messaging/context/package-info.java  |  20 +-
 .../plugin/oidc/op/storage/RevocationCache.java    |   3 +
 .../oidc/op/storage/RevocationCacheContexts.java   |  19 +-
 .../oidc/op/storage/RevocationCacheTest.java       |   1 -
 .../FormOutboundIntrospectionResponseMessage.java  | 169 ++++---------
 ...ndTokenIntrospectionResponseMessageContext.java |  10 +-
 .../profile/impl/ProcessTokenForIntrospection.java | 261 +++++++++++++++++++++
 .../oidc/op/oauth2/profile/impl/RevokeToken.java   |   2 +-
 ...ctInitializeOutboundResponseMessageContext.java |  27 ++-
 .../plugin/oidc/op/profile/impl/ValidateGrant.java |   2 +-
 .../impl/ValidateRegistrationAccessToken.java      |   2 +-
 .../userinfo/profile/impl/ValidateAccessToken.java |   2 +-
 .../META-INF/net.shibboleth.idp/postconfig.xml     |   7 +
 .../oauth2/introspection/introspection-beans.xml   |  16 +-
 .../oauth2/introspection/introspection-flow.xml    |   1 +
 .../flows/oauth2/revocation/revocation-beans.xml   |   2 +-
 .../flows/oidc/abstract/oidc-abstract-beans.xml    |   5 -
 .../idp/flows/oidc/register/register-beans.xml     |   2 +-
 .../idp/flows/oidc/token/token-beans.xml           |   2 +-
 .../idp/flows/oidc/userinfo/userinfo-beans.xml     |   2 +-
 .../idp/service/relying-party/postconfig.xml       |  90 ++++++-
 .../op/oauth2/profile/impl/RevokeTokenTest.java    |   2 +-
 .../op/profile/flow/IntrospectionFlowTest.java     |   3 +-
 .../profile/impl/BaseOIDCResponseActionTest.java   |   2 +-
 25 files changed, 528 insertions(+), 187 deletions(-)

diff --git a/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/oauth2/messaging/context/OAuth2TokenIntrospectionResponseContext.java b/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/oauth2/messaging/context/OAuth2TokenIntrospectionResponseContext.java
new file mode 100644
index 00000000..21b3a96e
--- /dev/null
+++ b/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/oauth2/messaging/context/OAuth2TokenIntrospectionResponseContext.java
@@ -0,0 +1,63 @@
+/*
+ * 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.oauth2.messaging.context;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.messaging.context.BaseContext;
+import org.opensaml.messaging.context.MessageContext;
+
+import com.nimbusds.jwt.JWTClaimsSet;
+
+/**
+ * Subcontext carrying information for an OAuth token introspection response.
+ * 
+ * <p>This context appears as a subcontext of a {@link MessageContext}.</p>
+ * 
+ * @since 3.1.0
+ */
+public class OAuth2TokenIntrospectionResponseContext extends BaseContext {
+
+    /** The token claim set. */
+    @Nullable private JWTClaimsSet tokenClaimsSet;
+
+    /**
+     * Get the token claims set.
+     * 
+     * @return token claims set
+     */
+    @Nullable public JWTClaimsSet getTokenClaimSet() {
+        return tokenClaimsSet;
+    }
+
+    /**
+     * Set the access token claims set (used when prepping OAuth-only access tokens).
+     * 
+     * @param claims token claims set
+     * 
+     * @return this context
+     */
+    @Nonnull public OAuth2TokenIntrospectionResponseContext setTokenClaimsSet(
+            @Nullable final JWTClaimsSet claims) {
+        tokenClaimsSet = claims;
+        
+        return this;
+    }
+
+}
\ No newline at end of file
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/InitializeOutboundTokenIntrospectionResponseMessageContext.java b/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/oauth2/messaging/context/package-info.java
similarity index 53%
copy from idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/InitializeOutboundTokenIntrospectionResponseMessageContext.java
copy to idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/oauth2/messaging/context/package-info.java
index 8c6ed7aa..5bf7ea2e 100644
--- a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/InitializeOutboundTokenIntrospectionResponseMessageContext.java
+++ b/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/oauth2/messaging/context/package-info.java
@@ -15,22 +15,6 @@
  * limitations under the License.
  */
 
-package net.shibboleth.idp.plugin.oidc.op.oauth2.profile.impl;
 
-import org.opensaml.messaging.context.MessageContext;
-import org.opensaml.profile.context.ProfileRequestContext;
-
-import net.shibboleth.idp.plugin.oidc.op.profile.impl.AbstractInitializeOutboundResponseMessageContext;
-
-/**
- * Action that adds an outbound {@link MessageContext} and related OIDC contexts to the {@link ProfileRequestContext}
- * not knowing the relying party yet.
- * 
- * TODO: This class can be eliminated now that generics are gone.
- *
- * @event {@link org.opensaml.profile.action.EventIds#PROCEED_EVENT_ID}
- */
-public class InitializeOutboundTokenIntrospectionResponseMessageContext
-        extends AbstractInitializeOutboundResponseMessageContext {
-
-}
\ No newline at end of file
+/** Context classes supporting OAuth2 profiles. */
+package net.shibboleth.idp.plugin.oidc.op.oauth2.messaging.context;
\ No newline at end of file
diff --git a/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/storage/RevocationCache.java b/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/storage/RevocationCache.java
index c637f1ba..8f356414 100644
--- a/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/storage/RevocationCache.java
+++ b/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/storage/RevocationCache.java
@@ -47,7 +47,10 @@ import org.slf4j.LoggerFactory;
  * This class is thread-safe and uses a synchronized method to prevent race conditions within the underlying store
  * (lacking an atomic "check and insert" operation).
  * </p>
+ * 
+ * @deprecated
  */
+ at Deprecated(since="3.1.0", forRemoval=true)
 @ThreadSafeAfterInit
 public class RevocationCache extends AbstractIdentifiableInitializableComponent {
 
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 6416ed8e..99e6890e 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
@@ -21,23 +21,24 @@ import javax.annotation.Nonnull;
 
 import net.shibboleth.utilities.java.support.annotation.constraint.NotEmpty;
 
-/** Revocation cache contexts shared across actions. */
+/**
+ * Revocation cache contexts shared across actions.
+ */
 public final class RevocationCacheContexts {
 
     /**
      * ID of context for revoking authorization codes (and access/refresh tokens based on the authorization codes).
+     * 
+     * <p>For historical reasons this also is used for directly issued access tokens.</p>
      */
-    @Nonnull
-    @NotEmpty
-    public static final String AUTHORIZATION_CODE = RevocationCacheContexts.class.getName() + ".AUTHORIZATION_CODE";
-
+    @Nonnull @NotEmpty public static final String AUTHORIZATION_CODE =
+            RevocationCacheContexts.class.getName() + ".AUTHORIZATION_CODE";
+    
     /**
      * ID of context for revoking access tokens issued for the dynamic client registration.
      */
-    @Nonnull
-    @NotEmpty
-    public static final String REGISTRATION_ACCESS_TOKEN = RevocationCacheContexts.class.getName()
-        + ".REGISTRATION_ACCESS_TOKEN";
+    @Nonnull @NotEmpty public static final String REGISTRATION_ACCESS_TOKEN =
+            RevocationCacheContexts.class.getName() + ".REGISTRATION_ACCESS_TOKEN";
 
     /** Private constructor. */
     private RevocationCacheContexts() {
diff --git a/idp-oidc-extension-api/src/test/java/net/shibboleth/idp/plugin/oidc/op/storage/RevocationCacheTest.java b/idp-oidc-extension-api/src/test/java/net/shibboleth/idp/plugin/oidc/op/storage/RevocationCacheTest.java
index 43e0900e..d48cdfc5 100644
--- a/idp-oidc-extension-api/src/test/java/net/shibboleth/idp/plugin/oidc/op/storage/RevocationCacheTest.java
+++ b/idp-oidc-extension-api/src/test/java/net/shibboleth/idp/plugin/oidc/op/storage/RevocationCacheTest.java
@@ -25,7 +25,6 @@ import org.opensaml.storage.impl.client.ClientStorageService;
 import org.testng.annotations.AfterMethod;
 import org.testng.annotations.Test;
 
-import net.shibboleth.idp.plugin.oidc.op.storage.RevocationCache;
 import net.shibboleth.utilities.java.support.component.ComponentInitializationException;
 import net.shibboleth.utilities.java.support.logic.ConstraintViolationException;
 
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/FormOutboundIntrospectionResponseMessage.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/FormOutboundIntrospectionResponseMessage.java
index bfcbf9d6..46f40e99 100644
--- a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/FormOutboundIntrospectionResponseMessage.java
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/FormOutboundIntrospectionResponseMessage.java
@@ -18,39 +18,27 @@
 package net.shibboleth.idp.plugin.oidc.op.oauth2.profile.impl;
 
 import java.text.ParseException;
-import java.util.Collections;
-import java.util.List;
 import java.util.stream.Collectors;
 
 import javax.annotation.Nonnull;
-import javax.annotation.Nullable;
 
 import org.opensaml.profile.context.ProfileRequestContext;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
+
+import com.nimbusds.jwt.JWTClaimsSet;
+import com.nimbusds.oauth2.sdk.Scope;
 import com.nimbusds.oauth2.sdk.TokenIntrospectionRequest;
 import com.nimbusds.oauth2.sdk.TokenIntrospectionSuccessResponse;
 import com.nimbusds.oauth2.sdk.id.Audience;
+import com.nimbusds.oauth2.sdk.id.ClientID;
 import com.nimbusds.oauth2.sdk.id.Issuer;
 import com.nimbusds.oauth2.sdk.id.Subject;
-import com.nimbusds.oauth2.sdk.token.AccessToken;
 import com.nimbusds.oauth2.sdk.token.AccessTokenType;
-import com.nimbusds.oauth2.sdk.token.RefreshToken;
-import com.nimbusds.oauth2.sdk.token.Token;
 
+import net.shibboleth.idp.plugin.oidc.op.oauth2.messaging.context.OAuth2TokenIntrospectionResponseContext;
 import net.shibboleth.idp.plugin.oidc.op.profile.impl.AbstractOIDCRequestAction;
-import net.shibboleth.idp.plugin.oidc.op.storage.RevocationCache;
-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;
-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.security.DataSealer;
-import net.shibboleth.utilities.java.support.security.DataSealerException;
 
 /**
  * Action that forms outbound token introspection success message. Formed message is set to
@@ -61,125 +49,62 @@ public class FormOutboundIntrospectionResponseMessage extends AbstractOIDCReques
     /** Class logger. */
     @Nonnull private Logger log = LoggerFactory.getLogger(FormOutboundIntrospectionResponseMessage.class);
 
-    /** Data sealer for unwrapping token. */
-    @NonnullAfterInit private DataSealer dataSealer;
-
-    /** Message revocation cache instance to use. */
-    @NonnullAfterInit private RevocationCache revocationCache;
-
-    /**
-     * Set the data sealer instance to use.
-     * 
-     * @param sealer data sealer to use
-     */
-    public void setDataSealer(@Nonnull final DataSealer sealer) {
-        ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
-        dataSealer = Constraint.isNotNull(sealer, "DataSealer cannot be null");
-    }
-    
-    /**
-     * 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");
-    }
-
-    /** {@inheritDoc} */
-    @Override
-    protected void doInitialize() throws ComponentInitializationException {
-        super.doInitialize();
-        
-        if (revocationCache == null || dataSealer == null) {
-            throw new ComponentInitializationException("RevocationCache and DataSealer cannot be null");
-        }
-    }
-
     /** {@inheritDoc} */
-    @Override
-    protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
+    @Override protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
         
-        log.debug("{} Token to introspect: {}", getLogPrefix(), getRequest().getToken().getValue());
+        final OAuth2TokenIntrospectionResponseContext ctx =
+                profileRequestContext.getOutboundMessageContext().getSubcontext(
+                        OAuth2TokenIntrospectionResponseContext.class);
         
-        final TokenClaimsSet tokenClaimsSet = parseToken(getRequest().getToken());
-        if (tokenClaimsSet == null) {
-            log.debug("{} Unable to decode token", getLogPrefix());
-            profileRequestContext.getOutboundMessageContext()
-                    .setMessage(new TokenIntrospectionSuccessResponse.Builder(false).build());
-            return;
-        }
-
-        log.debug("{} {} token unsealed: {}", getLogPrefix(),
-                tokenClaimsSet instanceof AccessTokenClaimsSet ? "Access" : "Refresh", tokenClaimsSet.serialize());
-        
-        if (!tokenClaimsSet.isTimeValid()) {
-            log.debug("{} Token ID {} is expired or future dated", getLogPrefix(), tokenClaimsSet.getID());
-            profileRequestContext.getOutboundMessageContext().setMessage(
-                    new TokenIntrospectionSuccessResponse.Builder(false).build());
-            return;
-        } else if (revocationCache.isRevoked(RevocationCacheContexts.AUTHORIZATION_CODE, tokenClaimsSet.getID())) {
-            log.debug("{} Token ID {} is revoked", getLogPrefix(), tokenClaimsSet.getID());
+        if (ctx == null || ctx.getTokenClaimSet() == null) {
+            log.debug("{} Introspection of token failed, token was not valid", getLogPrefix());
             profileRequestContext.getOutboundMessageContext().setMessage(
                     new TokenIntrospectionSuccessResponse.Builder(false).build());
             return;
         }
 
-        List<String> audiences = tokenClaimsSet.getAudience();
-        if (audiences == null || audiences.isEmpty()) {
-            audiences = Collections.singletonList(tokenClaimsSet.getClaimsSet().getIssuer());
-        }
-        
-        profileRequestContext.getOutboundMessageContext().setMessage(
-                new TokenIntrospectionSuccessResponse.Builder(true)
-                        .scope(tokenClaimsSet.getScope())
-                        .clientID(tokenClaimsSet.getClientID())
-                        .username(tokenClaimsSet.getPrincipal())
-                        .tokenType(AccessTokenType.BEARER)
-                        .expirationTime(tokenClaimsSet.getClaimsSet().getExpirationTime())
-                        .issueTime(tokenClaimsSet.getClaimsSet().getIssueTime())
-                        .subject(new Subject(tokenClaimsSet.getClaimsSet().getSubject()))
-                        .issuer(new Issuer(tokenClaimsSet.getClaimsSet().getIssuer()))
-                        .audience(audiences.stream().map(Audience::new).collect(Collectors.toUnmodifiableList()))
-                        .build());
-    }
+        final JWTClaimsSet tokenClaimsSet = ctx.getTokenClaimSet();
 
-    /**
-     * Attempt to parse token.
-     * 
-     * @param token the token
-     * 
-     * @return parsed claim set or null
-     */
-    @Nullable protected TokenClaimsSet parseToken(@Nonnull @NotEmpty final Token token) {
         try {
-            if (token instanceof AccessToken) {
-                return AccessTokenClaimsSet.parse(token.getValue(), dataSealer);
-            } else if (token instanceof RefreshToken) {
-                return RefreshTokenClaimsSet.parse(token.getValue(), dataSealer);
+            String clientID = tokenClaimsSet.getStringClaim(TokenClaimsSet.KEY_CLIENTID);
+            if (clientID == null) {
+                clientID = tokenClaimsSet.getStringClaim(TokenClaimsSet.KEY_LEGACY_CLIENTID);
             }
-        } catch (final DataSealerException | ParseException e) {
-            log.debug("{} Token to introspect is invalid or unknown", getLogPrefix());
-            return null;
-        }
-        
-        // Token type hint missing, have to try both.
-        try {
-            return AccessTokenClaimsSet.parse(token.getValue(), dataSealer);
-        } catch (final DataSealerException | ParseException e) {
+
+            final TokenIntrospectionSuccessResponse.Builder builder =
+                    new TokenIntrospectionSuccessResponse.Builder(true)
+                        .clientID(new ClientID(clientID))
+                        .tokenType(AccessTokenType.BEARER)
+                        .expirationTime(tokenClaimsSet.getExpirationTime())
+                        .issueTime(tokenClaimsSet.getIssueTime())
+                        .subject(new Subject(tokenClaimsSet.getSubject()))
+                        .issuer(new Issuer(tokenClaimsSet.getIssuer()));
             
-        }
-        
-        try {
-            return RefreshTokenClaimsSet.parse(token.getValue(), dataSealer);
-        } catch (final DataSealerException | ParseException e) {
+            String claim = tokenClaimsSet.getStringClaim(TokenClaimsSet.KEY_USER_PRINCIPAL);
+            if (claim != null) {
+                builder.username(claim);
+            }
             
+            claim = tokenClaimsSet.getStringClaim(TokenClaimsSet.KEY_SCOPE);
+            if (claim != null) {
+                builder.scope(Scope.parse(claim));
+            }
+            
+            if (!tokenClaimsSet.getAudience().isEmpty()) {
+                builder.audience(tokenClaimsSet.getAudience()
+                        .stream()
+                        .map(Audience::new)
+                        .collect(Collectors.toUnmodifiableList()));
+
+            }
+            
+            profileRequestContext.getOutboundMessageContext().setMessage(builder.build());
+            
+        } catch (final ParseException e) {
+            log.error("{} Failure extracting claims for response", getLogPrefix(), e);
+            profileRequestContext.getOutboundMessageContext().setMessage(
+                    new TokenIntrospectionSuccessResponse.Builder(false).build());
         }
-        
-        log.debug("{} Token to introspect is invalid or unknown", getLogPrefix());
-        
-        return null;
     }
 
 }
\ No newline at end of file
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/InitializeOutboundTokenIntrospectionResponseMessageContext.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/InitializeOutboundTokenIntrospectionResponseMessageContext.java
index 8c6ed7aa..0776a1a9 100644
--- a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/InitializeOutboundTokenIntrospectionResponseMessageContext.java
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/InitializeOutboundTokenIntrospectionResponseMessageContext.java
@@ -20,17 +20,21 @@ package net.shibboleth.idp.plugin.oidc.op.oauth2.profile.impl;
 import org.opensaml.messaging.context.MessageContext;
 import org.opensaml.profile.context.ProfileRequestContext;
 
+import net.shibboleth.idp.plugin.oidc.op.oauth2.messaging.context.OAuth2TokenIntrospectionResponseContext;
 import net.shibboleth.idp.plugin.oidc.op.profile.impl.AbstractInitializeOutboundResponseMessageContext;
 
 /**
- * Action that adds an outbound {@link MessageContext} and related OIDC contexts to the {@link ProfileRequestContext}
+ * Action that adds an outbound {@link MessageContext} and related contexts to the {@link ProfileRequestContext}
  * not knowing the relying party yet.
- * 
- * TODO: This class can be eliminated now that generics are gone.
  *
  * @event {@link org.opensaml.profile.action.EventIds#PROCEED_EVENT_ID}
  */
 public class InitializeOutboundTokenIntrospectionResponseMessageContext
         extends AbstractInitializeOutboundResponseMessageContext {
 
+    /** Constructor. */
+    public InitializeOutboundTokenIntrospectionResponseMessageContext() {
+        setContextType(OAuth2TokenIntrospectionResponseContext.class);
+    }
+
 }
\ No newline at end of file
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/ProcessTokenForIntrospection.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/ProcessTokenForIntrospection.java
new file mode 100644
index 00000000..4cf70f0b
--- /dev/null
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/ProcessTokenForIntrospection.java
@@ -0,0 +1,261 @@
+/*
+ * 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.oauth2.profile.impl;
+
+import java.text.ParseException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.function.Function;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.messaging.context.MessageContext;
+import org.opensaml.profile.action.EventIds;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.opensaml.security.credential.Credential;
+import org.opensaml.security.credential.CredentialResolver;
+import org.opensaml.security.credential.UsageType;
+import org.opensaml.security.criteria.UsageCriterion;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.nimbusds.jwt.JWTClaimsSet;
+import com.nimbusds.jwt.SignedJWT;
+import com.nimbusds.oauth2.sdk.TokenIntrospectionRequest;
+import com.nimbusds.oauth2.sdk.token.AccessToken;
+import com.nimbusds.oauth2.sdk.token.RefreshToken;
+import com.nimbusds.oauth2.sdk.token.Token;
+
+import net.shibboleth.idp.plugin.oidc.op.oauth2.messaging.context.OAuth2TokenIntrospectionResponseContext;
+import net.shibboleth.idp.plugin.oidc.op.profile.impl.AbstractOIDCRequestAction;
+import net.shibboleth.idp.plugin.oidc.op.token.support.AccessTokenClaimsSet;
+import net.shibboleth.idp.plugin.oidc.op.token.support.RefreshTokenClaimsSet;
+import net.shibboleth.idp.profile.ActionSupport;
+import net.shibboleth.idp.profile.IdPEventIds;
+import net.shibboleth.oidc.jwt.claims.ClaimsValidator;
+import net.shibboleth.oidc.jwt.claims.JWTValidationException;
+import net.shibboleth.oidc.profile.config.navigate.IssuedClaimsValidatorLookupFunction;
+import net.shibboleth.oidc.profile.core.OidcEventIds;
+import net.shibboleth.oidc.security.impl.JWTSignatureValidationUtil;
+import net.shibboleth.utilities.java.support.annotation.constraint.NotEmpty;
+import net.shibboleth.utilities.java.support.component.ComponentSupport;
+import net.shibboleth.utilities.java.support.logic.Constraint;
+import net.shibboleth.utilities.java.support.resolver.CriteriaSet;
+import net.shibboleth.utilities.java.support.resolver.ResolverException;
+import net.shibboleth.utilities.java.support.security.DataSealer;
+import net.shibboleth.utilities.java.support.security.DataSealerException;
+
+/**
+ * Action that processes a token for introspection by validating it and populating the resulting {@link JWTClaimsSet}
+ * into an {@link OAuth2TokenIntrospectionResponseContext} placed beneath the outbound {@link MessageContext}.
+ * 
+ * <p>If the token is invalid, revoked, or unintelligible, the context is not created or populated.</p>
+ * 
+ * @since 3.1.0
+ *      
+ * @pre ProfileRequestContext.getInboundMessageContext().getMessage() instanceof {@link TokenIntrospectionRequest}
+ * @post If the token is valid for the requester, ProfileRequestContext.getOutboundMessageContext().getSubcontext(
+ *      OAuth2TokenIntrospectionResponseContext.class) != null and the context contains the token's
+ *      {@link JWTClaimsSet}.
+ * @event {@link EventIds#PROCEED_EVENT_ID}
+ * @event {@link IdPEventIds#INVALID_PROFILE_CONFIG}
+ */
+public class ProcessTokenForIntrospection extends AbstractOIDCRequestAction<TokenIntrospectionRequest> {
+
+    /** Class logger. */
+    @Nonnull private Logger log = LoggerFactory.getLogger(ProcessTokenForIntrospection.class);
+
+    /** Data sealer for unwrapping token. */
+    @Nullable private DataSealer dataSealer;
+    
+    /** Lookup strategy for claims validator. */
+    @Nonnull private Function<ProfileRequestContext,ClaimsValidator> claimsValidatorLookupStrategy;
+    
+    /** The claims validator to use. */
+    @Nullable private ClaimsValidator claimsValidator;
+    
+    /** Source of signing keys. */
+    @Nullable private CredentialResolver credentialResolver;
+    
+    /** Copy of signed JWT for non-opaque access tokens. */
+    @Nullable private SignedJWT signedJWT;
+
+    /** Constructor. */
+    public ProcessTokenForIntrospection() {
+        claimsValidatorLookupStrategy = new IssuedClaimsValidatorLookupFunction();
+    }
+    
+    /**
+     * Set the data sealer instance to use.
+     * 
+     * @param sealer data sealer to use
+     */
+    public void setDataSealer(@Nullable final DataSealer sealer) {
+        ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+        dataSealer = sealer;
+    }
+    
+    /**
+     * Set the claims validator lookup strategy.
+     * 
+     * @param strategy lookup strategy
+     */
+    public void setClaimsValidatorLookupStrategy(
+            @Nonnull final Function<ProfileRequestContext,ClaimsValidator> strategy) {
+        ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+        claimsValidatorLookupStrategy = Constraint.isNotNull(strategy, "Lookup strategy cannot be null");
+    }
+    
+    /**
+     * Set the source of signing keys to use for JWT signature verification.
+     * 
+     * @param resolver signing key resolver
+     */
+    public void setCredentialResolver(@Nullable final CredentialResolver resolver) {
+        ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+        credentialResolver = resolver;
+    }
+        
+    /** {@inheritDoc} */
+    @Override
+    protected boolean doPreExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
+        if (!super.doPreExecute(profileRequestContext)) {
+            return false;
+        }
+        
+        claimsValidator = claimsValidatorLookupStrategy.apply(profileRequestContext);
+        if (claimsValidator == null) {
+            log.error("{} Unable to obtain ClaimsValidator to apply", getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, IdPEventIds.INVALID_PROFILE_CONFIG);
+            return false;
+        }
+        
+        return true;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
+        
+        log.debug("{} Token to introspect: {}", getLogPrefix(), getRequest().getToken().getValue());
+        
+        JWTClaimsSet tokenClaimsSet;
+        if (getRequest().getToken() instanceof AccessToken) {
+            tokenClaimsSet = parseAccessToken(getRequest().getToken());
+        } else if (getRequest().getToken() instanceof RefreshToken) {
+            tokenClaimsSet = parseRefreshToken(getRequest().getToken());
+        } else {
+            // No token hint, have to try both.
+            tokenClaimsSet = parseAccessToken(getRequest().getToken());
+            if (tokenClaimsSet == null) {
+                tokenClaimsSet = parseRefreshToken(getRequest().getToken());
+            }
+        }
+        
+        if (tokenClaimsSet == null) {
+            log.warn("{} Unable to parse/decode token for introspection", getLogPrefix());
+            return;
+        }
+
+        if (signedJWT != null) {
+            if (credentialResolver == null) {
+                log.error("{} No CredentialResolver available, can't verify JWT signature", getLogPrefix());
+                return;
+            }
+            
+            log.debug("{} Checking JWT signature", getLogPrefix());
+            final Collection<Credential> credList = new ArrayList<>();
+            final CriteriaSet criteriaSet = new CriteriaSet(new UsageCriterion(UsageType.SIGNING));
+            try {
+                final Iterable<Credential> creds = credentialResolver.resolve(criteriaSet);
+                if (creds != null) {
+                    creds.forEach(credList::add);
+                }
+            } catch (final ResolverException e) {
+                log.error("{} Failure resolving signing credentials, can't verify JWT signature", getLogPrefix(), e);
+                return;
+            }
+            final String errorEventId = JWTSignatureValidationUtil.validateSignatureEx(credList, signedJWT,
+                    OidcEventIds.INVALID_GRANT);
+            if (errorEventId != null) {
+                log.warn("{} Signature on token ID '{}' invalid", getLogPrefix(), tokenClaimsSet.getJWTID());
+                return;
+            }
+        }
+
+        log.debug("{} Validating parsed/decoded claims set: {}", getLogPrefix(), tokenClaimsSet.toString());
+        try {
+            claimsValidator.validate(tokenClaimsSet, profileRequestContext);
+        } catch (final JWTValidationException e) {
+            log.warn("{} Claims validation failed, token is invalid", getLogPrefix(), e.getMessage());
+            return;
+        }
+        
+        // Populate outbound tree.
+        profileRequestContext.getOutboundMessageContext().getSubcontext(
+                OAuth2TokenIntrospectionResponseContext.class).setTokenClaimsSet(tokenClaimsSet);
+    }
+
+    /**
+     * Attempt to parse token.
+     * 
+     * @param token the token
+     * 
+     * @return parsed claim set or null
+     */
+    @Nullable protected JWTClaimsSet parseAccessToken(@Nonnull @NotEmpty final Token token) {
+        
+        // Try parsing as a JWT.
+        try {
+            signedJWT = SignedJWT.parse(token.getValue());
+            return signedJWT.getJWTClaimsSet();
+        } catch (final ParseException e1) {
+            
+        }
+
+        // Fall back to opaque.
+        try {
+            return AccessTokenClaimsSet.parse(token.getValue(), dataSealer).getClaimsSet();
+        } catch (final DataSealerException | ParseException e) {
+            
+        }
+        
+        return null;
+    }
+
+    /**
+     * Attempt to parse refresh token.
+     * 
+     * @param token the token
+     * 
+     * @return parsed claim set or null
+     */
+    @Nullable protected JWTClaimsSet parseRefreshToken(@Nonnull @NotEmpty final Token token) {
+
+        // All refresh tokens are opaque.
+        try {
+            return RefreshTokenClaimsSet.parse(token.getValue(), dataSealer).getClaimsSet();
+        } catch (final DataSealerException | ParseException e) {
+            
+        }
+        
+        return null;
+    }
+
+}
\ No newline at end of file
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 b9287039..dead3564 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
@@ -21,12 +21,12 @@ import java.text.ParseException;
 import javax.annotation.Nonnull;
 
 import org.opensaml.profile.context.ProfileRequestContext;
+import org.opensaml.storage.RevocationCache;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import com.nimbusds.oauth2.sdk.TokenRevocationRequest;
 
 import net.shibboleth.idp.plugin.oidc.op.profile.impl.AbstractOIDCRequestAction;
-import net.shibboleth.idp.plugin.oidc.op.storage.RevocationCache;
 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;
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/AbstractInitializeOutboundResponseMessageContext.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/AbstractInitializeOutboundResponseMessageContext.java
index 0e30ea2d..0e082b36 100644
--- a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/AbstractInitializeOutboundResponseMessageContext.java
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/AbstractInitializeOutboundResponseMessageContext.java
@@ -21,7 +21,10 @@ import javax.annotation.Nonnull;
 
 import net.shibboleth.idp.plugin.oidc.op.messaging.context.OIDCAuthenticationResponseContext;
 import net.shibboleth.idp.profile.AbstractProfileAction;
+import net.shibboleth.utilities.java.support.component.ComponentSupport;
+import net.shibboleth.utilities.java.support.logic.Constraint;
 
+import org.opensaml.messaging.context.BaseContext;
 import org.opensaml.messaging.context.MessageContext;
 import org.opensaml.profile.context.ProfileRequestContext;
 import org.slf4j.Logger;
@@ -35,16 +38,34 @@ import org.slf4j.LoggerFactory;
 public abstract class AbstractInitializeOutboundResponseMessageContext extends AbstractProfileAction {
 
     /** Class logger. */
-    @Nonnull
-    private final Logger log = LoggerFactory.getLogger(AbstractInitializeOutboundResponseMessageContext.class);
+    @Nonnull private final Logger log = LoggerFactory.getLogger(AbstractInitializeOutboundResponseMessageContext.class);
 
+    /** Type of subcontext to create. */
+    @Nonnull private Class<? extends BaseContext> contextType;
+    
+    /** Constructor. */
+    public AbstractInitializeOutboundResponseMessageContext() {
+        contextType = OIDCAuthenticationResponseContext.class;
+    }
+    
+    /**
+     * Set the type of subcontext to create.
+     * 
+     * @param claz context type
+     */
+    public void setContextType(@Nonnull final Class<? extends BaseContext> claz) {
+        ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+        
+        contextType = Constraint.isNotNull(claz, "Context type cannot be null");
+    }
+    
     /** {@inheritDoc} */
     @Override
     protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
 
         final MessageContext msgCtx = new MessageContext();
         profileRequestContext.setOutboundMessageContext(msgCtx);
-        msgCtx.addSubcontext(new OIDCAuthenticationResponseContext());
+        msgCtx.getSubcontext(contextType, true);
         log.debug("{} Initialized outbound message context", getLogPrefix());
     }
 
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 29fe3931..ad81535a 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
@@ -27,6 +27,7 @@ import org.opensaml.messaging.context.navigate.ChildContextLookup;
 import org.opensaml.profile.action.ActionSupport;
 import org.opensaml.profile.context.ProfileRequestContext;
 import org.opensaml.storage.ReplayCache;
+import org.opensaml.storage.RevocationCache;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -37,7 +38,6 @@ import com.nimbusds.oauth2.sdk.RefreshTokenGrant;
 
 import net.shibboleth.idp.plugin.oidc.op.messaging.context.OIDCAuthenticationResponseContext;
 import net.shibboleth.idp.plugin.oidc.op.profile.OidcEventIds;
-import net.shibboleth.idp.plugin.oidc.op.storage.RevocationCache;
 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;
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/ValidateRegistrationAccessToken.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/ValidateRegistrationAccessToken.java
index 342e4a8e..20b48040 100644
--- a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/ValidateRegistrationAccessToken.java
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/ValidateRegistrationAccessToken.java
@@ -27,6 +27,7 @@ import org.opensaml.messaging.context.navigate.ChildContextLookup;
 import org.opensaml.profile.action.ActionSupport;
 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,7 +37,6 @@ import com.nimbusds.openid.connect.sdk.rp.OIDCClientRegistrationRequest;
 
 import net.shibboleth.idp.plugin.oidc.op.messaging.context.OIDCClientRegistrationTokenClaimsContext;
 import net.shibboleth.idp.plugin.oidc.op.profile.OidcEventIds;
-import net.shibboleth.idp.plugin.oidc.op.storage.RevocationCache;
 import net.shibboleth.idp.plugin.oidc.op.storage.RevocationCacheContexts;
 import net.shibboleth.idp.plugin.oidc.op.token.support.RegistrationClaimsSet;
 import net.shibboleth.idp.profile.context.RelyingPartyContext;
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/userinfo/profile/impl/ValidateAccessToken.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/userinfo/profile/impl/ValidateAccessToken.java
index a72cf6f3..196962f9 100644
--- a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/userinfo/profile/impl/ValidateAccessToken.java
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/userinfo/profile/impl/ValidateAccessToken.java
@@ -23,12 +23,12 @@ import javax.annotation.Nonnull;
 
 import org.opensaml.profile.action.ActionSupport;
 import org.opensaml.profile.context.ProfileRequestContext;
+import org.opensaml.storage.RevocationCache;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 import net.shibboleth.idp.plugin.oidc.op.messaging.context.OIDCAuthenticationResponseContext;
 import net.shibboleth.idp.plugin.oidc.op.profile.OidcEventIds;
-import net.shibboleth.idp.plugin.oidc.op.storage.RevocationCache;
 import net.shibboleth.idp.plugin.oidc.op.storage.RevocationCacheContexts;
 import net.shibboleth.idp.plugin.oidc.op.token.support.AccessTokenClaimsSet;
 import net.shibboleth.utilities.java.support.annotation.constraint.NonnullAfterInit;
diff --git a/idp-oidc-extension-impl/src/main/resources/META-INF/net.shibboleth.idp/postconfig.xml b/idp-oidc-extension-impl/src/main/resources/META-INF/net.shibboleth.idp/postconfig.xml
index 34cab869..d3c65f34 100644
--- a/idp-oidc-extension-impl/src/main/resources/META-INF/net.shibboleth.idp/postconfig.xml
+++ b/idp-oidc-extension-impl/src/main/resources/META-INF/net.shibboleth.idp/postconfig.xml
@@ -64,6 +64,13 @@
     <bean id="base64Codec" class="org.apache.commons.codec.binary.Base64" c:lineLength="0"
         c:lineSeparator="#{new byte[] {10} }" c:urlSafe="true" />
 
+    <!-- Revocation cache. -->
+    
+    <bean id="shibboleth.oidc.RevocationCache" class="org.opensaml.storage.RevocationCache"
+        p:entryExpiration="#{'%{idp.oidc.revocationCache.authorizeCode.lifetime:PT6H}'}"
+        p:storage-ref="#{'%{idp.oidc.revocationCache.StorageService:shibboleth.StorageService}'.trim()}"
+        p:strict="true" />
+
     <!-- OIDC client information resolver service beans. -->
 
     <bean id="shibboleth.ClientInformationResolverService"
diff --git a/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oauth2/introspection/introspection-beans.xml b/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oauth2/introspection/introspection-beans.xml
index 2d15f09e..87c139a4 100644
--- a/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oauth2/introspection/introspection-beans.xml
+++ b/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oauth2/introspection/introspection-beans.xml
@@ -25,13 +25,21 @@
         class="net.shibboleth.idp.plugin.oidc.op.oauth2.profile.impl.InitializeOutboundTokenIntrospectionResponseMessageContext"
         scope="prototype" />
 
-    <bean id="FormOutboundMessage"
-        class="net.shibboleth.idp.plugin.oidc.op.oauth2.profile.impl.FormOutboundIntrospectionResponseMessage" scope="prototype"
+    <bean id="ProcessTokenForIntrospection"
+        class="net.shibboleth.idp.plugin.oidc.op.oauth2.profile.impl.ProcessTokenForIntrospection" scope="prototype"
         p:dataSealer-ref="#{'%{idp.oidc.tokenSealer:shibboleth.oidc.TokenSealer}'.trim()}"
-        p:revocationCache-ref="shibboleth.RevocationCache" />
+        p:credentialResolver-ref="SigningCredentialsResolver" />
+        
+    <bean id="SigningCredentialsResolver" class="net.shibboleth.idp.relyingparty.impl.SigningCredentialsResolver" 
+        c:_0-ref="shibboleth.RelyingPartyResolverService" />
+
+    <bean id="FormOutboundMessage"
+        class="net.shibboleth.idp.plugin.oidc.op.oauth2.profile.impl.FormOutboundIntrospectionResponseMessage"
+        scope="prototype" />
 
     <bean id="BuildErrorResponseFromEvent"
-        class="net.shibboleth.idp.plugin.oidc.op.oauth2.profile.impl.BuildIntrospectionErrorResponseFromEvent" scope="prototype"
+        class="net.shibboleth.idp.plugin.oidc.op.oauth2.profile.impl.BuildIntrospectionErrorResponseFromEvent"
+        scope="prototype"
             p:httpServletResponse-ref="shibboleth.HttpServletResponse"
             p:mappedErrors-ref="shibboleth.oidc.ErrorMappings">
         <property name="eventContextLookupStrategy">
diff --git a/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oauth2/introspection/introspection-flow.xml b/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oauth2/introspection/introspection-flow.xml
index 5df00ffd..dbdd7c1d 100644
--- a/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oauth2/introspection/introspection-flow.xml
+++ b/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oauth2/introspection/introspection-flow.xml
@@ -31,6 +31,7 @@
     <!-- Authentication subflow happens here. -->
 
     <action-state id="ResumeAfterAuthentication">
+        <evaluate expression="ProcessTokenForIntrospection" />
         <evaluate expression="'proceed'" />
         <transition on="proceed" to="BuildResponseMessage" />
     </action-state>
diff --git a/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oauth2/revocation/revocation-beans.xml b/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oauth2/revocation/revocation-beans.xml
index 18db2847..167452b8 100644
--- a/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oauth2/revocation/revocation-beans.xml
+++ b/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oauth2/revocation/revocation-beans.xml
@@ -27,7 +27,7 @@
 
     <bean id="RevokeToken" class="net.shibboleth.idp.plugin.oidc.op.oauth2.profile.impl.RevokeToken" scope="prototype"
         p:dataSealer-ref="#{'%{idp.oidc.tokenSealer:shibboleth.oidc.TokenSealer}'.trim()}"
-        p:revocationCache-ref="shibboleth.RevocationCache" />
+        p:revocationCache-ref="shibboleth.oidc.RevocationCache" />
 
     <bean id="FormOutboundMessage"
         class="net.shibboleth.idp.plugin.oidc.op.oauth2.profile.impl.FormOutboundRevokeTokenResponseMessage"
diff --git a/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/abstract/oidc-abstract-beans.xml b/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/abstract/oidc-abstract-beans.xml
index 09510db0..48676a1f 100644
--- a/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/abstract/oidc-abstract-beans.xml
+++ b/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/abstract/oidc-abstract-beans.xml
@@ -25,11 +25,6 @@
     <bean id="shibboleth.oidc.TokenSignatureSigningParametersResolver"
         class="net.shibboleth.idp.plugin.oidc.op.security.impl.OIDCClientInformationSignatureSigningParametersResolver" />
 
-    <bean id="shibboleth.RevocationCache" class="net.shibboleth.idp.plugin.oidc.op.storage.RevocationCache"
-        depends-on="shibboleth.LoggingService"
-        p:entryExpiration="#{'%{idp.oidc.revocationCache.authorizeCode.lifetime:PT6H}'}"
-        p:storage-ref="#{'%{idp.oidc.revocationCache.StorageService:shibboleth.StorageService}'.trim()}" p:strict="true" />
-
     <bean id="SelectRelyingPartyConfiguration"
         class="net.shibboleth.idp.profile.impl.SelectRelyingPartyConfiguration" scope="prototype"
         p:relyingPartyConfigurationResolver-ref="shibboleth.RelyingPartyConfigurationResolver" />
diff --git a/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/register/register-beans.xml b/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/register/register-beans.xml
index 1a305cd0..f2b7f68a 100644
--- a/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/register/register-beans.xml
+++ b/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/register/register-beans.xml
@@ -26,7 +26,7 @@
 
     <bean id="ValidateRegistrationAccessToken"
         class="net.shibboleth.idp.plugin.oidc.op.profile.impl.ValidateRegistrationAccessToken" scope="prototype"
-        p:revocationCache-ref="shibboleth.RevocationCache"
+        p:revocationCache-ref="shibboleth.oidc.RevocationCache"
         p:sealer-ref="#{'%{idp.oidc.dynreg.tokenSealer:shibboleth.oidc.TokenSealer}'.trim()}"
         p:objectMapper-ref="shibboleth.JSONObjectMapper" />
         
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 02955e6c..68b305ad 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
@@ -80,7 +80,7 @@
     <bean id="ValidateGrant" class="net.shibboleth.idp.plugin.oidc.op.profile.impl.ValidateGrant" scope="prototype"
         c:sealer-ref="#{'%{idp.oidc.tokenSealer:shibboleth.oidc.TokenSealer}'.trim()}"
         p:replayCache-ref="shibboleth.ReplayCache"
-        p:revocationCache-ref="shibboleth.RevocationCache" />
+        p:revocationCache-ref="shibboleth.oidc.RevocationCache" />
     
     <bean id="ValidatePKCE" class="net.shibboleth.idp.plugin.oidc.op.profile.impl.ValidatePKCE" scope="prototype" />
 
diff --git a/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/userinfo/userinfo-beans.xml b/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/userinfo/userinfo-beans.xml
index ffe6b9b5..cd162031 100644
--- a/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/userinfo/userinfo-beans.xml
+++ b/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/userinfo/userinfo-beans.xml
@@ -24,7 +24,7 @@
     <bean id="ValidateAccessToken"
         class="net.shibboleth.idp.plugin.oidc.op.userinfo.profile.impl.ValidateAccessToken" scope="prototype"
         p:dataSealer-ref="#{'%{idp.oidc.tokenSealer:shibboleth.oidc.TokenSealer}'.trim()}"
-        p:revocationCache-ref="shibboleth.RevocationCache" />
+        p:revocationCache-ref="shibboleth.oidc.RevocationCache" />
 
     <bean id="shibboleth.ClientIDLookupStrategy"
         class="net.shibboleth.idp.plugin.oidc.op.profile.context.navigate.UserInfoRequestClientIDLookupFunction" />
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 d1d4e920..d14da7d0 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
@@ -58,19 +58,21 @@
         class="net.shibboleth.oidc.profile.config.OIDCProviderInformationConfiguration"
         p:issuer-ref="issuer" />
         
-    <bean id="OAUTH2.Revocation" parent="AbstractOIDCProfile" lazy-init="true"
-        class="net.shibboleth.oidc.profile.oauth2.config.OAuth2TokenRevocationConfiguration"
-        p:tokenEndpointAuthMethods="%{idp.oidc.dynreg.tokenEndpointAuthMethods:client_secret_basic,client_secret_post,client_secret_jwt,private_key_jwt}"
-        p:claimsValidator-ref="DefaultJWTClaimsValidator" />
-        
     <bean id="OIDC.Keyset" parent="AbstractOIDCProfile" lazy-init="true"
         class="net.shibboleth.oidc.profile.config.OIDCPublishKeySetConfiguration"
         p:securityConfiguration-ref="shibboleth.oidc.PublishKeySetSecurityConfiguration" />
         
     <bean id="OAUTH2.Introspection" parent="AbstractOIDCProfile" lazy-init="true"
         class="net.shibboleth.oidc.profile.oauth2.config.OAuth2TokenIntrospectionConfiguration"
+        p:issuer-ref="issuer"
         p:tokenEndpointAuthMethods="%{idp.oidc.dynreg.tokenEndpointAuthMethods:client_secret_basic,client_secret_post,client_secret_jwt,private_key_jwt}"
-        p:claimsValidator-ref="DefaultJWTClaimsValidator" />
+        p:claimsValidator-ref="DefaultIssuedJWTClaimsValidator" />
+
+    <bean id="OAUTH2.Revocation" parent="AbstractOIDCProfile" lazy-init="true"
+        class="net.shibboleth.oidc.profile.oauth2.config.OAuth2TokenRevocationConfiguration"
+        p:issuer-ref="issuer"
+        p:tokenEndpointAuthMethods="%{idp.oidc.dynreg.tokenEndpointAuthMethods:client_secret_basic,client_secret_post,client_secret_jwt,private_key_jwt}"
+        p:claimsValidator-ref="DefaultIssuedJWTClaimsValidator" />        
 
     <!-- Metadata-driven variants. -->
     
@@ -110,6 +112,17 @@
                 p:propertyType="#{T(net.shibboleth.oidc.authn.principal.AuthenticationContextClassReferencePrincipal)}" />
         </property>
     </bean>
+
+    <bean id="AbstractMDDrivenOAuthTokenValidatingProfile" parent="AbstractMDDrivenOAuthClientAuthenticatableProfile" abstract="true">
+        <property name="issuerLookupStrategy">
+            <bean parent="shibboleth.MDDrivenStringProperty" p:propertyName="issuer" p:defaultValue-ref="issuer" />
+        </property>
+        <property name="issuedClaimsValidatorLookupStrategy">
+            <bean parent="shibboleth.MDDrivenBeanProperty" p:propertyName="issuedClaimsValidator"
+                p:propertyType="#{T(net.shibboleth.oidc.jwt.claims.ClaimsValidator)}"
+                p:defaultValue-ref="DefaultIssuedJWTClaimsValidator" />
+        </property>
+    </bean>
         
     <bean id="AbstractMDDrivenOIDCFlowAwareProfile" parent="AbstractMDDrivenOAuthClientAuthenticatableProfile" abstract="true">
         <property name="authorizationCodeFlowEnabledPredicate">
@@ -339,7 +352,7 @@
         </property>
     </bean>
             
-    <bean id="OAUTH2.Revocation.MDDriven" parent="AbstractMDDrivenOAuthClientAuthenticatableProfile" lazy-init="true"
+    <bean id="OAUTH2.Revocation.MDDriven" parent="AbstractMDDrivenOAuthTokenValidatingProfile" lazy-init="true"
             class="net.shibboleth.oidc.profile.oauth2.config.OAuth2TokenRevocationConfiguration">
         <property name="tokenEndpointAuthMethodsLookupStrategy">
             <bean parent="shibboleth.MDDrivenSetProperty" p:propertyName="tokenEndpointAuthMethods">
@@ -353,7 +366,7 @@
         </property>
     </bean>
 
-    <bean id="OAUTH2.Introspection.MDDriven" parent="AbstractMDDrivenOAuthClientAuthenticatableProfile" lazy-init="true"
+    <bean id="OAUTH2.Introspection.MDDriven" parent="AbstractMDDrivenOAuthTokenValidatingProfile" lazy-init="true"
             class="net.shibboleth.oidc.profile.oauth2.config.OAuth2TokenIntrospectionConfiguration">
         <property name="tokenEndpointAuthMethodsLookupStrategy">
             <bean parent="shibboleth.MDDrivenSetProperty" p:propertyName="tokenEndpointAuthMethods">
@@ -367,7 +380,7 @@
         </property>
     </bean>
 
-    <!-- Default JWT validation wiring. -->
+    <!-- Default client-auth JWT validation wiring. -->
 
     <bean id="DefaultJWTClaimsValidator"
         class="net.shibboleth.oidc.security.jwt.claims.impl.ChainingJWTClaimsValidator"
@@ -380,6 +393,10 @@
         class="net.shibboleth.oidc.security.jwt.claims.impl.ExpiryClaimsValidator"
         p:clockSkew="%{idp.policy.clockSkew:PT1M}" />
 
+    <bean id="NotBeforeClaimsValidator"
+        class="net.shibboleth.oidc.security.jwt.claims.impl.NotBeforeClaimsValidator"
+        p:clockSkew="%{idp.policy.clockSkew:PT1M}" />
+        
     <bean id="IssuedAtClaimsValidator"
         class="net.shibboleth.oidc.security.jwt.claims.impl.IssuedAtClaimsValidator"
         p:clockSkew="%{idp.policy.clockSkew:PT1M}"
@@ -410,6 +427,7 @@
 
     <util:list id="ClaimsValidators" value-type="net.shibboleth.oidc.jwt.claims.ClaimsValidator">
         <ref bean="ExpiryClaimsValidator" />
+        <ref bean="NotBeforeClaimsValidator" />
         <ref bean="IssuedAtClaimsValidator" />
         <ref bean="IssuerClaimsValidator" />
         <ref bean="SubjectClaimsValidator" />
@@ -417,6 +435,56 @@
         <ref bean="JWTIdentifierClaimsValidator" />
     </util:list>
 
+    <!-- Default issued JWT validation wiring (for introspection/revocation). -->
+
+    <bean id="DefaultIssuedJWTClaimsValidator"
+        class="net.shibboleth.oidc.security.jwt.claims.impl.ChainingJWTClaimsValidator"
+        p:claimValidators-ref="IssuedClaimsValidators" />
+
+    <bean id="SelfIssuedClaimsValidator"
+            class="net.shibboleth.oidc.security.jwt.claims.impl.ExactMatchClaimsValidator"
+            p:claimName="iss">
+        <property name="valueToMatchLookupStrategy">
+            <bean class="net.shibboleth.utilities.java.support.logic.BiFunctionSupport"
+                factory-method="forFunctionOfFirstArg"
+                    c:_0-ref="shibboleth.ResponderIdLookup.Simple" />
+        </property>
+    </bean>
+
+    <bean id="ClientIDClaimsValidator"
+        class="net.shibboleth.oidc.security.jwt.claims.impl.ExactMatchClaimsValidator"
+        p:claimName="client_id" p:valueToMatchLookupStrategy-ref="ClientIDFromOIDCMetadataContextLookupFunction" />
+
+    <bean id="LegacyClientIDClaimsValidator"
+        class="net.shibboleth.oidc.security.jwt.claims.impl.ExactMatchClaimsValidator"
+        p:claimName="clid" p:valueToMatchLookupStrategy-ref="ClientIDFromOIDCMetadataContextLookupFunction" />
+
+    <bean id="ClientIDInAudienceClaimsValidator"
+        class="net.shibboleth.oidc.security.jwt.claims.impl.AudienceClaimsValidator"
+        p:audienceLookupStrategy-ref="ClientIDFromOIDCMetadataContextLookupFunction" />
+
+    <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).AUTHORIZATION_CODE}" />
+
+    <util:list id="IssuedClaimsValidators" value-type="net.shibboleth.oidc.jwt.claims.ClaimsValidator">
+        <ref bean="ExpiryClaimsValidator" />
+        <ref bean="NotBeforeClaimsValidator" />
+        <ref bean="SelfIssuedClaimsValidator" />
+        <!-- For issued tokens, ensure that the requester is either the client_id or the audience. -->
+        <bean class="net.shibboleth.oidc.security.jwt.claims.impl.ChainingJWTClaimsValidator" p:requireAll="false">
+            <property name="claimValidators">
+                <list value-type="net.shibboleth.oidc.jwt.claims.ClaimsValidator">
+                    <ref bean="ClientIDClaimsValidator" />
+                    <ref bean="LegacyClientIDClaimsValidator" />
+                    <ref bean="ClientIDInAudienceClaimsValidator" />
+                </list>
+            </property>
+        </bean>
+        <ref bean="JWTIDRevocationClaimsValidator" />
+    </util:list>
+
     <!--
     Auto-wiring exposers for credentials to get them loaded into the IdP's relying party config resolver.
     The qualifiers control which auto-wiring point is used.
@@ -672,8 +740,8 @@
     <bean id="shibboleth.oidc.dynreg.BatchCacheBuilder" factory-bean="batchCacheFactory" factory-method="build"
         abstract="true"/>
 
-    <bean class="net.shibboleth.oidc.metadata.cache.impl.BatchMetadataCacheBuilderSpec" 
-        id="shibboleth.oidc.dynreg.BatchMetadataCacheBuilderSpec" abstract="true" 
+    <bean id="shibboleth.oidc.dynreg.BatchMetadataCacheBuilderSpec" abstract="true"
+        class="net.shibboleth.oidc.metadata.cache.impl.BatchMetadataCacheBuilderSpec"
         p:parsingStrategy-ref="shibboleth.oidc.dynreg.DefaultJSONMapParsingStrategy"
         p:criteriaToIdentifierStrategy-ref="shibboleth.oidc.dynreg.DefaultMetadataCriteriaToIdentifierStrategy"
         p:sourceMetadataExpiryStrategy-ref="shibboleth.oidc.dynreg.DefaultExpirationTimeStrategy"
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 dfb24911..82b7eb76 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
@@ -22,6 +22,7 @@ import java.net.URISyntaxException;
 import java.security.NoSuchAlgorithmException;
 import java.time.Duration;
 
+import org.opensaml.storage.RevocationCache;
 import org.opensaml.storage.impl.MemoryStorageService;
 import org.springframework.webflow.execution.RequestContext;
 import org.testng.Assert;
@@ -33,7 +34,6 @@ import com.nimbusds.oauth2.sdk.TokenRevocationRequest;
 import com.nimbusds.oauth2.sdk.id.ClientID;
 import com.nimbusds.oauth2.sdk.token.BearerAccessToken;
 
-import net.shibboleth.idp.plugin.oidc.op.storage.RevocationCache;
 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;
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 2ca1a8fc..a93d71b4 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
@@ -110,7 +110,8 @@ public class IntrospectionFlowTest extends AbstractOidcClientAuthenticationFlowT
                 parseSuccessResponse(result, TokenIntrospectionSuccessResponse.class);
         Assert.assertTrue(resp.isActive());
         Assert.assertEquals(resp.getClientID().getValue(), clientId);
-        Assert.assertEquals(resp.getAudience(), Collections.singletonList(new Audience("https://op.example.org")));
+        Assert.assertEquals(resp.getScope(), Scope.parse("openid"));
+        Assert.assertNull(resp.getAudience());
     }
 
     @Test
diff --git a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/BaseOIDCResponseActionTest.java b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/BaseOIDCResponseActionTest.java
index a77c2325..3c94ef42 100644
--- a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/BaseOIDCResponseActionTest.java
+++ b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/BaseOIDCResponseActionTest.java
@@ -28,7 +28,6 @@ import javax.annotation.Nonnull;
 import net.shibboleth.idp.plugin.oidc.op.messaging.context.OIDCAuthenticationResponseContext;
 import net.shibboleth.idp.plugin.oidc.op.messaging.context.OIDCMetadataContext;
 import net.shibboleth.idp.plugin.oidc.op.profile.spring.factory.BasicJWKCredentialFactoryBean;
-import net.shibboleth.idp.plugin.oidc.op.storage.RevocationCache;
 import net.shibboleth.idp.profile.context.RelyingPartyContext;
 import net.shibboleth.idp.profile.context.navigate.WebflowRequestContextProfileRequestContextLookup;
 import net.shibboleth.idp.profile.testing.RequestContextBuilder;
@@ -43,6 +42,7 @@ import net.shibboleth.utilities.java.support.security.impl.SecureRandomIdentifie
 import org.opensaml.messaging.context.MessageContext;
 import org.opensaml.profile.context.ProfileRequestContext;
 import org.opensaml.security.credential.Credential;
+import org.opensaml.storage.RevocationCache;
 import org.springframework.core.io.ClassPathResource;
 import org.springframework.webflow.execution.RequestContext;
 import org.testng.annotations.BeforeMethod;

-- 
To stop receiving notification emails like this one, please contact
the administrator of this repository.


More information about the commits mailing list