[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