[java-idp-oidc] branch main updated: JOIDC-7 - Support JWT access tokens for code or implicit grants
Scott Cantor
cantor.2 at osu.edu
Wed Apr 27 14:30:37 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=4fd9d227f58b9be2fd3b5a82758c674d321785ab
The following commit(s) were added to refs/heads/main by this push:
new 4fd9d227 JOIDC-7 - Support JWT access tokens for code or implicit grants
4fd9d227 is described below
commit 4fd9d227f58b9be2fd3b5a82758c674d321785ab
Author: Scott Cantor <cantor.2 at osu.edu>
AuthorDate: Wed Apr 27 10:29:20 2022 -0400
JOIDC-7 - Support JWT access tokens for code or implicit grants
https://shibboleth.atlassian.net/browse/JOIDC-7
Working patch for code and implicit.
Also removed redirect_uri claim from access tokens.
---
.../AbstractTokenClaimsLookupFunction.java | 2 +-
.../navigate/ClientInfoAudienceLookupFunction.java | 3 +
.../DefaultRequestAudienceLookupFunction.java | 69 ++++
.../DefaultResponseClaimsSetLookupFunction.java | 2 +-
...uthenticationResponseContextLookupFunction.java | 4 +-
.../TokenRequestRedirectURILookupFunction.java | 2 +-
.../op/token/support/AccessTokenClaimsSet.java | 48 ++-
.../op/token/support/RefreshTokenClaimsSet.java | 2 +-
.../oidc/op/token/support/TokenClaimsSet.java | 3 +
.../op/oauth2/profile/impl/BuildAccessToken.java | 320 ++++++++++++++---
.../op/oauth2/profile/impl/ValidateAudience.java | 63 +++-
.../impl/AbstractOIDCTokenResponseAction.java | 2 +-
.../profile/impl/AddAccessTokenHashToIDToken.java | 3 +-
.../op/profile/impl/AddAttributesToClaimsSet.java | 1 -
.../impl/SetAccessTokenToResponseContext.java | 332 ------------------
.../SetAuthorizationCodeToResponseContext.java | 21 +-
...liveryAttributesFromTokenToResponseContext.java | 2 +-
.../plugin/oidc/op/profile/impl/ValidateGrant.java | 6 +-
.../oidc/op/profile/impl/ValidateRedirectURI.java | 2 +-
.../plugin/oidc/op/profile/impl/ValidateScope.java | 6 +-
...uteConsentEnabledInTokenClaimsSetPredicate.java | 2 +-
.../impl/BuildUserInfoErrorResponseFromEvent.java | 4 +-
.../impl/FormOutboundUserInfoResponseMessage.java | 3 +-
...lizeOutboundUserInfoResponseMessageContext.java | 4 +-
.../op/userinfo/profile/impl/ParseAccessToken.java | 203 +++++++++++
.../profile/impl/SignUserInfoResponse.java | 3 +-
.../userinfo/profile/impl/ValidateAccessToken.java | 116 +++----
.../META-INF/net.shibboleth.idp/postconfig.xml | 1 +
.../idp/flows/oidc/authorize/authorize-beans.xml | 385 +++++++++++++++------
.../idp/flows/oidc/authorize/authorize-flow.xml | 141 +++++++-
.../idp/flows/oidc/token/token-beans.xml | 163 +++++----
.../shibboleth/idp/flows/oidc/token/token-flow.xml | 107 ++++--
.../idp/flows/oidc/userinfo/userinfo-beans.xml | 28 +-
.../idp/flows/oidc/userinfo/userinfo-flow.xml | 3 +-
.../idp/service/relying-party/postconfig.xml | 46 ++-
.../oauth2/profile/impl/BuildAccessTokenTest.java | 13 +-
.../oidc/op/profile/flow/AbstractOidcFlowTest.java | 2 +-
.../oidc/op/profile/flow/AuthorizeFlowTest.java | 219 +++++++++---
.../plugin/oidc/op/profile/flow/TokenFlowTest.java | 7 +-
.../profile/impl/BaseOIDCResponseActionTest.java | 10 +-
.../impl/SetAccessTokenToResponseContextTest.java | 283 ---------------
.../FormOutboundUserInfoResponseMessageTest.java | 3 +-
...essTokenTest.java => ParseAccessTokenTest.java} | 190 +++++-----
.../profile/impl/SignUserInfoResponseTest.java | 3 +-
.../profile/impl/ValidateAccessTokenTest.java | 138 +++-----
pom.xml | 4 +-
46 files changed, 1731 insertions(+), 1243 deletions(-)
diff --git a/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/context/navigate/AbstractTokenClaimsLookupFunction.java b/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/context/navigate/AbstractTokenClaimsLookupFunction.java
index f74cc2e9..94dc3e1e 100644
--- a/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/context/navigate/AbstractTokenClaimsLookupFunction.java
+++ b/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/context/navigate/AbstractTokenClaimsLookupFunction.java
@@ -50,7 +50,7 @@ public abstract class AbstractTokenClaimsLookupFunction<T>
return null;
}
final OIDCAuthenticationResponseContext oidcResponseContext =
- input.getOutboundMessageContext().getSubcontext(OIDCAuthenticationResponseContext.class, false);
+ input.getOutboundMessageContext().getSubcontext(OIDCAuthenticationResponseContext.class);
if (oidcResponseContext == null) {
return null;
}
diff --git a/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/context/navigate/ClientInfoAudienceLookupFunction.java b/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/context/navigate/ClientInfoAudienceLookupFunction.java
index 9ae72310..e4170897 100644
--- a/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/context/navigate/ClientInfoAudienceLookupFunction.java
+++ b/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/context/navigate/ClientInfoAudienceLookupFunction.java
@@ -20,6 +20,7 @@ package net.shibboleth.idp.plugin.oidc.op.profile.context.navigate;
import java.net.URI;
import java.util.ArrayList;
import java.util.Collection;
+import java.util.Collections;
import java.util.List;
import javax.annotation.Nonnull;
@@ -70,6 +71,8 @@ public class ClientInfoAudienceLookupFunction implements ContextDataLookupFuncti
}
return audience;
+ } else if (obj instanceof String) {
+ return Collections.singletonList((String) obj);
}
return null;
diff --git a/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/context/navigate/DefaultRequestAudienceLookupFunction.java b/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/context/navigate/DefaultRequestAudienceLookupFunction.java
new file mode 100644
index 00000000..b8b72b50
--- /dev/null
+++ b/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/context/navigate/DefaultRequestAudienceLookupFunction.java
@@ -0,0 +1,69 @@
+/*
+ * Licensed to the University Corporation for Advanced Internet Development,
+ * Inc. (UCAID) under one or more contributor license agreements. See the
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID licenses this file to You under the Apache
+ * License, Version 2.0 (the "License"); you may not use this file except in
+ * compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.idp.plugin.oidc.op.profile.context.navigate;
+
+import java.net.URI;
+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.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import com.nimbusds.openid.connect.sdk.AuthenticationRequest;
+
+/**
+ * A function that returns resource value of the authentication request.
+ *
+ * @since 3.2.0
+ */
+public class DefaultRequestAudienceLookupFunction extends AbstractAuthenticationRequestLookupFunction<List<String>> {
+
+ /** Class logger. */
+ @Nonnull private Logger log = LoggerFactory.getLogger(DefaultRequestAudienceLookupFunction.class);
+
+ /** {@inheritDoc} */
+ @Nullable protected List<String> doLookup(@Nonnull final AuthenticationRequest req) {
+ try {
+ if (getRequestObject() != null && getRequestObject().getJWTClaimsSet().getClaim("resource") != null) {
+ final Object resource = getRequestObject().getJWTClaimsSet().getClaim("resource");
+ if (resource instanceof String) {
+ return Collections.singletonList((String) resource);
+ } else if (resource instanceof List) {
+ return (List<String>) resource;
+ } else if (resource != null) {
+ log.error("resource claim is not of expected type");
+ return null;
+ }
+
+ }
+ } catch (final ParseException e) {
+ log.error("Unable to parse request object");
+ return null;
+ }
+
+ return req.getResources() == null ? null :
+ req.getResources().stream()
+ .map(URI::toString)
+ .collect(Collectors.toUnmodifiableList());
+ }
+
+}
\ No newline at end of file
diff --git a/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/context/navigate/DefaultResponseClaimsSetLookupFunction.java b/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/context/navigate/DefaultResponseClaimsSetLookupFunction.java
index 3d00b22b..92fd6c03 100644
--- a/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/context/navigate/DefaultResponseClaimsSetLookupFunction.java
+++ b/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/context/navigate/DefaultResponseClaimsSetLookupFunction.java
@@ -36,7 +36,7 @@ public class DefaultResponseClaimsSetLookupFunction
return null;
}
final OIDCAuthenticationResponseContext ctx =
- input.getOutboundMessageContext().getSubcontext(OIDCAuthenticationResponseContext.class, false);
+ input.getOutboundMessageContext().getSubcontext(OIDCAuthenticationResponseContext.class);
if (ctx == null) {
return null;
}
diff --git a/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/context/navigate/OIDCAuthenticationResponseContextLookupFunction.java b/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/context/navigate/OIDCAuthenticationResponseContextLookupFunction.java
index 13eb87c3..b5ec3ed5 100644
--- a/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/context/navigate/OIDCAuthenticationResponseContextLookupFunction.java
+++ b/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/context/navigate/OIDCAuthenticationResponseContextLookupFunction.java
@@ -29,7 +29,7 @@ import net.shibboleth.idp.plugin.oidc.op.messaging.context.OIDCAuthenticationRes
* {@link ProfileRequestContext}.
*/
public class OIDCAuthenticationResponseContextLookupFunction
- implements ContextDataLookupFunction<ProfileRequestContext, OIDCAuthenticationResponseContext> {
+ implements ContextDataLookupFunction<ProfileRequestContext,OIDCAuthenticationResponseContext> {
/** {@inheritDoc} */
@Nullable
@@ -37,7 +37,7 @@ public class OIDCAuthenticationResponseContextLookupFunction
if (input == null || input.getOutboundMessageContext() == null) {
return null;
}
- return input.getOutboundMessageContext().getSubcontext(OIDCAuthenticationResponseContext.class, false);
+ return input.getOutboundMessageContext().getSubcontext(OIDCAuthenticationResponseContext.class);
}
}
diff --git a/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/context/navigate/TokenRequestRedirectURILookupFunction.java b/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/context/navigate/TokenRequestRedirectURILookupFunction.java
index 81f4ce6d..1b5b1015 100644
--- a/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/context/navigate/TokenRequestRedirectURILookupFunction.java
+++ b/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/context/navigate/TokenRequestRedirectURILookupFunction.java
@@ -57,7 +57,7 @@ public class TokenRequestRedirectURILookupFunction extends AbstractTokenRequestL
try {
uri = new URI(redirectURI);
} catch (final URISyntaxException e) {
- log.warn("Unable to parse uri from token request redirect_uri {}", redirectURI);
+ log.warn("Unable to parse URI from token request redirect_uri {}", redirectURI);
}
return uri;
}
diff --git a/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/token/support/AccessTokenClaimsSet.java b/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/token/support/AccessTokenClaimsSet.java
index e3ac61c4..0bce7967 100644
--- a/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/token/support/AccessTokenClaimsSet.java
+++ b/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/token/support/AccessTokenClaimsSet.java
@@ -20,6 +20,7 @@ package net.shibboleth.idp.plugin.oidc.op.token.support;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
+import com.nimbusds.jwt.JWT;
import com.nimbusds.jwt.JWTClaimsSet;
import com.nimbusds.oauth2.sdk.Scope;
import com.nimbusds.oauth2.sdk.id.ClientID;
@@ -34,6 +35,7 @@ import net.shibboleth.utilities.java.support.security.IdentifierGenerationStrate
import java.net.URI;
import java.text.ParseException;
import java.time.Instant;
+import java.util.Map;
/** Class wrapping claims set for access token. */
public final class AccessTokenClaimsSet extends TokenClaimsSet {
@@ -63,9 +65,9 @@ public final class AccessTokenClaimsSet extends TokenClaimsSet {
setClaimsSet(builder.buildJWTClaimsSet(VALUE_TYPE_AT));
}
// Checkstyle: ParameterNumber ON
-
+
/**
- * Private constructor for the parser.
+ * Direct constructor.
*
* @param accessTokenClaimsSet access token claims set
*/
@@ -102,6 +104,44 @@ public final class AccessTokenClaimsSet extends TokenClaimsSet {
return parse(dataSealer.unwrap(wrappedAccessToken));
}
+ /**
+ * Parses access token from sealed access token.
+ *
+ * @param jwtAccessToken wrapped access token
+ * @param dataSealer sealer to unwrap the access token
+ *
+ * @return access token claims set
+ *
+ * @throws ParseException is thrown if unwrapped access token is not understood
+ * @throws DataSealerException is thrown if unwrapping fails
+ *
+ * @since 3.2.0
+ */
+ @Nonnull public static AccessTokenClaimsSet parse(@Nonnull @NotEmpty final JWT jwtAccessToken,
+ @Nonnull final DataSealer dataSealer) throws ParseException, DataSealerException {
+
+ JWTClaimsSet claims = jwtAccessToken.getJWTClaimsSet();
+
+ // Check for embedded custom claim.
+ if (claims.getClaim(TokenClaimsSet.KEY_SEALED_FOR_OP) == null) {
+ // Throws exception if parsing result is not expected one.
+ verifyParsedClaims(VALUE_TYPE_AT, claims);
+ return new AccessTokenClaimsSet(claims);
+ }
+
+ final Map<String,Object> map = claims.toJSONObject();
+ final JWTClaimsSet unsealed = JWTClaimsSet.parse(
+ dataSealer.unwrap(claims.getStringClaim(TokenClaimsSet.KEY_SEALED_FOR_OP)));
+ map.remove(TokenClaimsSet.KEY_SEALED_FOR_OP);
+ for (Map.Entry<String,Object> claim : unsealed.getClaims().entrySet()) {
+ map.put(claim.getKey(), claim.getValue());
+ }
+
+ claims = JWTClaimsSet.parse(map);
+ verifyParsedClaims(VALUE_TYPE_AT, claims);
+ return new AccessTokenClaimsSet(claims);
+ }
+
/** Builder to create instance of AccessTokenClaimsSet. */
public static final class Builder extends TokenClaimsSet.Builder<AccessTokenClaimsSet> {
@@ -144,7 +184,6 @@ public final class AccessTokenClaimsSet extends TokenClaimsSet {
setIssuedAt(issuedAt);
setExpiresAt(expiresAt);
setAuthenticationTime(authenticationTime);
- setRedirectURI(redirectURI);
setScope(scope);
}
@@ -188,7 +227,6 @@ public final class AccessTokenClaimsSet extends TokenClaimsSet {
setNotBefore(existing.getNotBefore());
setAuthenticationTime(existing.getAuthenticationTime());
setAudience(existing.getAudience());
- setRedirectURI(existing.getRedirectURI());
setClaimsRequest(existing.getClaimsRequest());
setConsentedClaims(existing.getConsentedClaims());
setConsentEnabled(existing.isConsentEnabled());
@@ -204,7 +242,7 @@ public final class AccessTokenClaimsSet extends TokenClaimsSet {
public AccessTokenClaimsSet build() {
return new AccessTokenClaimsSet(buildJWTClaimsSet(VALUE_TYPE_AT));
}
-
+
}
}
diff --git a/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/token/support/RefreshTokenClaimsSet.java b/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/token/support/RefreshTokenClaimsSet.java
index 1bfbbf3b..682363e9 100644
--- a/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/token/support/RefreshTokenClaimsSet.java
+++ b/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/token/support/RefreshTokenClaimsSet.java
@@ -136,8 +136,8 @@ public final class RefreshTokenClaimsSet extends TokenClaimsSet {
setPrincipal(existing.getPrincipal());
setSubject(existing.getClaimsSet().getSubject());
setACR(existing.getACR() == null ? null : new ACR(existing.getACR()));
- setNonce(existing.getNonce());
setAuthenticationTime(existing.getAuthenticationTime());
+ setNonce(existing.getNonce());
setRedirectURI(existing.getRedirectURI());
setScope(existing.getScope());
setClaimsRequest(existing.getClaimsRequest());
diff --git a/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/token/support/TokenClaimsSet.java b/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/token/support/TokenClaimsSet.java
index 0c3b1790..ce81beb2 100644
--- a/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/token/support/TokenClaimsSet.java
+++ b/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/token/support/TokenClaimsSet.java
@@ -130,6 +130,9 @@ public class TokenClaimsSet {
/** Code Challenge. */
@Nonnull @NotEmpty public static final String KEY_CODE_CHALLENGE = "cc";
+ /** Custom claim name for sealed claims embedded inside JWT. */
+ @Nonnull @NotEmpty public static final String KEY_SEALED_FOR_OP = "for_op";
+
/** Claims set for the claim. */
@Nullable private JWTClaimsSet tokenClaimsSet;
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/BuildAccessToken.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/BuildAccessToken.java
index ba01d36c..55a783ed 100644
--- a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/BuildAccessToken.java
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/BuildAccessToken.java
@@ -17,9 +17,13 @@
package net.shibboleth.idp.plugin.oidc.op.oauth2.profile.impl;
+import java.text.ParseException;
import java.time.Duration;
import java.time.Instant;
+import java.util.HashMap;
+import java.util.Map;
import java.util.function.Function;
+import java.util.function.Predicate;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
@@ -31,25 +35,38 @@ import org.opensaml.profile.context.navigate.OutboundMessageContextLookup;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+import com.nimbusds.jwt.JWTClaimsSet;
import com.nimbusds.jwt.PlainJWT;
import com.nimbusds.oauth2.sdk.Scope;
import com.nimbusds.oauth2.sdk.id.ClientID;
+import com.nimbusds.openid.connect.sdk.AuthenticationRequest;
+import com.nimbusds.openid.connect.sdk.claims.ClaimsSet;
-import net.shibboleth.idp.attribute.context.AttributeContext;
+import net.minidev.json.JSONArray;
+import net.shibboleth.idp.authn.context.SubjectContext;
import net.shibboleth.idp.plugin.oidc.op.messaging.context.AccessTokenContext;
+import net.shibboleth.idp.plugin.oidc.op.messaging.context.OIDCAuthenticationResponseConsentContext;
import net.shibboleth.idp.plugin.oidc.op.messaging.context.OIDCAuthenticationResponseContext;
+import net.shibboleth.idp.plugin.oidc.op.messaging.context.OIDCAuthenticationResponseTokenClaimsContext;
+import net.shibboleth.idp.plugin.oidc.op.profile.context.navigate.OIDCAuthenticationResponseContextLookupFunction;
import net.shibboleth.idp.plugin.oidc.op.profile.context.navigate.TokenRequestClientIDLookupFunction;
import net.shibboleth.idp.plugin.oidc.op.profile.impl.AbstractOIDCResponseAction;
import net.shibboleth.idp.plugin.oidc.op.token.support.AccessTokenClaimsSet;
+import net.shibboleth.idp.plugin.oidc.op.token.support.AuthorizeCodeClaimsSet;
+import net.shibboleth.idp.plugin.oidc.op.token.support.RefreshTokenClaimsSet;
+import net.shibboleth.idp.plugin.oidc.op.token.support.TokenClaimsSet;
import net.shibboleth.idp.plugin.oidc.op.token.support.AccessTokenClaimsSet.Builder;
import net.shibboleth.idp.profile.IdPEventIds;
import net.shibboleth.idp.profile.context.navigate.ResponderIdLookupFunction;
+import net.shibboleth.oidc.profile.config.logic.AttributeConsentFlowEnabledPredicate;
import net.shibboleth.oidc.profile.config.navigate.AccessTokenLifetimeLookupFunction;
import net.shibboleth.oidc.profile.config.navigate.AccessTokenTypeLookupFunction;
import org.opensaml.messaging.context.navigate.ChildContextLookup;
import org.opensaml.profile.action.ActionSupport;
+import net.shibboleth.utilities.java.support.annotation.constraint.NonnullAfterInit;
+import net.shibboleth.utilities.java.support.component.ComponentInitializationException;
import net.shibboleth.utilities.java.support.component.ComponentSupport;
import net.shibboleth.utilities.java.support.logic.Constraint;
import net.shibboleth.utilities.java.support.logic.FunctionSupport;
@@ -60,12 +77,13 @@ import net.shibboleth.utilities.java.support.security.impl.SecureRandomIdentifie
/**
* Action that creates an Access Token, and stores it to an {@link AccessTokenContext}.
+ *
+ * <p>There are various cases handled across different grant types and orders of operation.
+ * The token may be produced solely for a third-party service to consume, or may also or instead
+ * be usable with the OP's UserInfo endpoint.</p>
*
- * <p>This supports arbitrary ("generic") OAuth access tokens requested by the "client_credentials" grant type.
- * This is a "pure" OAuth use case and does not involve any OIDC behavior.</p>
- *
- * <p>The action supports either fully opaque access tokens sealed under the IdP's secret key, or
- * the RFC 9068 standard for JWT-based tokens.</p>
+ * <p>The action supports either opaque access tokens sealed under the IdP's secret key, or the
+ * RFC 9068 standard for JWT-based tokens.</p>
*
* @event {@link EventIds#PROCEED_EVENT_ID}
* @event {@link EventIds#MESSAGE_PROC_ERROR}
@@ -84,7 +102,7 @@ public class BuildAccessToken extends AbstractOIDCResponseAction {
@Nonnull private Logger log = LoggerFactory.getLogger(BuildAccessToken.class);
/** Sealer to use for opaque tokens. */
- @Nullable private DataSealer dataSealer;
+ @NonnullAfterInit private DataSealer dataSealer;
/** Strategy used to obtain the response issuer value. */
@Nonnull private Function<ProfileRequestContext,String> issuerLookupStrategy;
@@ -101,21 +119,38 @@ public class BuildAccessToken extends AbstractOIDCResponseAction {
/** Strategy used to locate the {@link IdentifierGenerationStrategy} to use. */
@Nonnull private Function<ProfileRequestContext,IdentifierGenerationStrategy> idGeneratorLookupStrategy;
+ /** Strategy used to locate the {@link OIDCAuthenticationResponseTokenClaimsContext}. */
+ @Nonnull private Function<ProfileRequestContext,OIDCAuthenticationResponseTokenClaimsContext>
+ tokenClaimsContextLookupStrategy;
+
+ /** Strategy used to locate the {@link OIDCAuthenticationResponseConsentContext}. */
+ @Nonnull private Function<ProfileRequestContext, OIDCAuthenticationResponseConsentContext>
+ consentContextLookupStrategy;
+
+ /** Predicate used to check if consent is enabled with a given {@link ProfileRequestContext}. */
+ @Nonnull private Predicate<ProfileRequestContext> consentEnabledPredicate;
+
/** Strategy used to create the subcontext to hold the token. */
@Nonnull private Function<ProfileRequestContext,AccessTokenContext> accessTokenContextCreationStrategy;
+
+ /** Authorize Code / Refresh Token the access token is based on, if any. */
+ @Nullable private TokenClaimsSet tokenClaimsSet;
+
+ /** Authentication request in the case of such. */
+ @Nullable private AuthenticationRequest authenticationRequest;
+
+ /** Subject context. */
+ @Nullable private SubjectContext subjectCtx;
/** Use a JWT for the token. */
private boolean jwtTokenType;
- /** Access token context. */
- @Nullable private AccessTokenContext accessTokenCtx;
-
- /** Attribute context. */
- @Nullable private AttributeContext attributeCtx;
-
/** The generator to use. */
@Nullable private IdentifierGenerationStrategy idGenerator;
+ /** Access token context. */
+ @Nullable private AccessTokenContext accessTokenCtx;
+
/** Constructor. */
public BuildAccessToken() {
accessTokenTypeLookupStrategy = new AccessTokenTypeLookupFunction();
@@ -127,6 +162,15 @@ public class BuildAccessToken extends AbstractOIDCResponseAction {
idGeneratorLookupStrategy = FunctionSupport.constant(new SecureRandomIdentifierGenerationStrategy());
+ tokenClaimsContextLookupStrategy =
+ new ChildContextLookup<>(OIDCAuthenticationResponseTokenClaimsContext.class).compose(
+ new OIDCAuthenticationResponseContextLookupFunction());
+ consentContextLookupStrategy =
+ new ChildContextLookup<>(OIDCAuthenticationResponseConsentContext.class).compose(
+ new OIDCAuthenticationResponseContextLookupFunction());
+
+ consentEnabledPredicate = new AttributeConsentFlowEnabledPredicate();
+
// PRC -> inbound message context -> OIDC response context -> ATC
accessTokenContextCreationStrategy = new ChildContextLookup<>(AccessTokenContext.class, true).compose(
new ChildContextLookup<>(OIDCAuthenticationResponseContext.class).compose(
@@ -182,38 +226,86 @@ public class BuildAccessToken extends AbstractOIDCResponseAction {
}
/**
- * Set the strategy used to create the {@link AccessTokenContext} to use.
+ * Set the strategy used to locate the issuer value to use.
*
- * @param strategy creation strategy
+ * @param strategy lookup strategy
*/
- public void setAccessTokenContextCreationStrategy(
- @Nonnull final Function<ProfileRequestContext,AccessTokenContext> strategy) {
+ public void setIssuerLookupStrategy(@Nonnull final Function<ProfileRequestContext,String> strategy) {
ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
- accessTokenContextCreationStrategy =
- Constraint.isNotNull(strategy, "AccessTokenContext creation strategy cannot be null");
+ issuerLookupStrategy = Constraint.isNotNull(strategy, "Issuer lookup strategy cannot be null");
}
/**
- * Set the strategy used to locate the issuer value to use.
+ * Set the strategy used to locate the original {@link ClientID} from the request.
*
* @param strategy lookup strategy
*/
- public void setIssuerLookupStrategy(@Nonnull final Function<ProfileRequestContext,String> strategy) {
+ public void setClientIDLookupStrategy(@Nonnull final Function<ProfileRequestContext,ClientID> strategy) {
ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
- issuerLookupStrategy = Constraint.isNotNull(strategy, "Issuer lookup strategy cannot be null");
+ clientIDLookupStrategy = Constraint.isNotNull(strategy, "ClientID lookup strategy cannot be null");
}
/**
- * Set the strategy used to locate the original {@link ClientID} from the request.
+ * Set the strategy used to locate the {@link OIDCAuthenticationResponseTokenClaimsContext} associated with a given
+ * {@link ProfileRequestContext}.
*
* @param strategy lookup strategy
*/
- public void setClientIDLookupStrategy(@Nonnull final Function<ProfileRequestContext,ClientID> strategy) {
+ public void setOIDCAuthenticationResponseTokenClaimsContextLookupStrategy(
+ @Nonnull final Function<ProfileRequestContext,OIDCAuthenticationResponseTokenClaimsContext> strategy) {
+ ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+ tokenClaimsContextLookupStrategy = Constraint.isNotNull(strategy,
+ "OIDCAuthenticationResponseTokenClaimsContextt lookup strategy cannot be null");
+ }
+
+ /**
+ * Set the strategy used to locate the {@link OIDCAuthenticationResponseConsentContext} associated with a given
+ * {@link ProfileRequestContext}.
+ *
+ * @param strategy lookup strategy
+ */
+ public void setOIDCAuthenticationResponseConsentContextLookupStrategy(
+ @Nonnull final Function<ProfileRequestContext,OIDCAuthenticationResponseConsentContext> strategy) {
+ ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+ consentContextLookupStrategy = Constraint.isNotNull(strategy,
+ "OIDCAuthenticationResponseConsentContext lookup strategy cannot be null");
+ }
+
+ /**
+ * Set the predicate used to check if consent is enabled with a given {@link ProfileRequestContext}.
+ *
+ * @param predicate predicate used to check if consent is enabled with a given {@link ProfileRequestContext}.
+ */
+ public void setConsentEnabledPredicate(@Nonnull final Predicate<ProfileRequestContext> predicate) {
+ ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+
+ consentEnabledPredicate =
+ Constraint.isNotNull(predicate, "predicate used to check if consent is enabled cannot be null");
+ }
+
+ /**
+ * Set the strategy used to create the {@link AccessTokenContext} to use.
+ *
+ * @param strategy creation strategy
+ */
+ public void setAccessTokenContextCreationStrategy(
+ @Nonnull final Function<ProfileRequestContext,AccessTokenContext> strategy) {
ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
- clientIDLookupStrategy = Constraint.isNotNull(strategy, "ClientID lookup strategy cannot be null");
+ accessTokenContextCreationStrategy =
+ Constraint.isNotNull(strategy, "AccessTokenContext creation strategy cannot be null");
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected void doInitialize() throws ComponentInitializationException {
+ super.doInitialize();
+
+ if (dataSealer == null) {
+ throw new ComponentInitializationException("DataSealer cannot be null");
+ }
}
/** {@inheritDoc} */
@@ -232,13 +324,42 @@ public class BuildAccessToken extends AbstractOIDCResponseAction {
return false;
}
- idGenerator = idGeneratorLookupStrategy.apply(profileRequestContext);
- if (idGenerator == null) {
- log.error("{} No identifier generation strategy", getLogPrefix());
+ tokenClaimsSet = getOidcResponseContext().getAuthorizationGrantClaimsSet();
+ if (tokenClaimsSet != null && !(tokenClaimsSet instanceof RefreshTokenClaimsSet)
+ && !(tokenClaimsSet instanceof AuthorizeCodeClaimsSet)) {
+ log.error("{} Authorization grant is of unknown type: {}", getLogPrefix(), tokenClaimsSet.getClass().getName());
ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
return false;
}
-
+
+ if (tokenClaimsSet == null) {
+ /*
+ * Typically this path applies when the client_credentials grant is used.
+ *
+ * Alternatively the access token may be provided by the authz endpoint without a code.
+ * This is the case only with the "token id_token" response type.
+ */
+ subjectCtx = profileRequestContext.getSubcontext(SubjectContext.class);
+ if (subjectCtx == null) {
+ log.error("{} No subject context", getLogPrefix());
+ ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
+ return false;
+ }
+
+ idGenerator = idGeneratorLookupStrategy.apply(profileRequestContext);
+ if (idGenerator == null) {
+ log.error("{} No identifier generation strategy", getLogPrefix());
+ ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
+ return false;
+ }
+
+ if (profileRequestContext.getInboundMessageContext() != null
+ && profileRequestContext.getInboundMessageContext().getMessage() instanceof AuthenticationRequest) {
+ authenticationRequest =
+ (AuthenticationRequest) profileRequestContext.getInboundMessageContext().getMessage();
+ }
+ }
+
accessTokenCtx = accessTokenContextCreationStrategy.apply(profileRequestContext);
if (accessTokenCtx == null) {
log.error("{} Unable to create AccessTokenContext", getLogPrefix());
@@ -268,27 +389,71 @@ public class BuildAccessToken extends AbstractOIDCResponseAction {
return;
}
+ ClaimsSet claims = null;
+ ClaimsSet claimsUI = null;
+ final OIDCAuthenticationResponseTokenClaimsContext tokenClaimsCtx =
+ tokenClaimsContextLookupStrategy.apply(profileRequestContext);
+ if (tokenClaimsCtx != null) {
+ claims = tokenClaimsCtx.getClaims();
+ claimsUI = tokenClaimsCtx.getUserinfoClaims();
+ }
+
final OIDCAuthenticationResponseContext responseCtx = getOidcResponseContext();
final Scope scope = responseCtx.getScope() != null ? responseCtx.getScope() : new Scope();
+ log.debug("{} Building access token with scope: {}", getLogPrefix(), scope);
+
+ final boolean oidc = scope.contains("openid");
+ if (oidc) {
+ responseCtx.getAudience().add(issuer);
+ }
log.debug("{} Building access token with audience: {}", getLogPrefix(), responseCtx.getAudience());
- log.debug("{} Building access token with scope: {}", getLogPrefix(), scope);
final Instant now = Instant.now();
final Instant dateExp = now.plus(accessTokenCtx.getLifetime());
- final AccessTokenClaimsSet.Builder builder = (Builder) new AccessTokenClaimsSet.Builder()
- .setJWTID(idGenerator)
- .setClientID(clientID)
- .setIssuer(issuer)
- .setSubject(responseCtx.getSubject())
- .setIssuedAt(now)
- .setExpiresAt(dateExp)
- .setACR(responseCtx.getAcr())
- .setAuthenticationTime(responseCtx.getAuthTime())
- .setScope(scope)
- .setAudience(responseCtx.getAudience());
+ final AccessTokenClaimsSet.Builder builder;
+
+ if (tokenClaimsSet != null) {
+ // We may not use original claims as input for scope / delivery claims as they may have been reduced.
+ builder = new AccessTokenClaimsSet.Builder(
+ tokenClaimsSet,
+ scope,
+ oidc ? claims : null,
+ oidc ? claimsUI : null,
+ Instant.now(),
+ dateExp);
+ // Add additional bits.
+ builder.setAudience(responseCtx.getAudience());
+ } else {
+ final OIDCAuthenticationResponseConsentContext consentCtx =
+ consentContextLookupStrategy.apply(profileRequestContext);
+ final JSONArray consented = consentCtx != null ? consentCtx.getConsentedAttributes() : null;
+
+ builder = (Builder) new AccessTokenClaimsSet.Builder()
+ .setJWTID(idGenerator)
+ .setClientID(clientID)
+ .setIssuer(issuer)
+ .setPrincipal(subjectCtx.getPrincipalName())
+ .setSubject(responseCtx.getSubject())
+ .setIssuedAt(now)
+ .setExpiresAt(dateExp)
+ .setACR(responseCtx.getAcr())
+ .setAuthenticationTime(responseCtx.getAuthTime())
+ .setScope(scope)
+ .setAudience(responseCtx.getAudience())
+ .setDlClaims(claims)
+ .setDlClaimsUI(claimsUI)
+ .setConsentedClaims(consented)
+ .setConsentEnabled(consentEnabledPredicate.test(profileRequestContext));
+
+ if (authenticationRequest != null) {
+ builder
+ .setNonce(authenticationRequest.getNonce())
+ .setClaimsRequest(authenticationRequest.getOIDCClaims());
+ }
+ }
if (jwtTokenType && responseCtx.getAccessTokenClaimSet() != null) {
builder.setCustomClaims(responseCtx.getAccessTokenClaimSet().toJSONObject());
@@ -298,18 +463,79 @@ public class BuildAccessToken extends AbstractOIDCResponseAction {
try {
if (jwtTokenType) {
- accessTokenCtx.setJWT(new PlainJWT(claimsSet.getClaimsSet()));
- log.debug("{} Claims stored to JWT access token: {}", getLogPrefix(), claimsSet.serialize(),
- accessTokenCtx.getJWT());
+ accessTokenCtx.setJWT(new PlainJWT(sealClaims(claimsSet.getClaimsSet())));
+ log.debug("{} Claims stored to JWT access token: {}", getLogPrefix(), claimsSet.serialize());
} else {
accessTokenCtx.setOpaque(claimsSet.serialize(dataSealer));
- log.debug("{} Claims '{}' converted to opaque access token: {}", getLogPrefix(), claimsSet.serialize(),
- accessTokenCtx.getOpaque());
+ log.debug("{} Claims converted to opaque access token: {}", getLogPrefix(), claimsSet.serialize());
}
- } catch (final DataSealerException e) {
+ } catch (final DataSealerException | ParseException e) {
log.error("{} Access Token wrapping failed: {}", getLogPrefix(), e);
ActionSupport.buildEvent(profileRequestContext, EventIds.MESSAGE_PROC_ERROR);
}
}
+ /**
+ * Rewrites a plaintext claimsset to hide custom claims used solely by the OP.
+ *
+ * @param claims the input claims
+ *
+ * @return a rewritten claims set to use for the access token
+ *
+ * @throws ParseException if unable to parse a claims set
+ * @throws DataSealerException if unable to seal the custom claims
+ */
+ @Nonnull private JWTClaimsSet sealClaims(@Nonnull final JWTClaimsSet claims) throws DataSealerException, ParseException {
+
+ // Rewrite as a mutable map.
+ final Map<String,Object> map = claims.toJSONObject();
+
+ // Put the claims to hide here.
+ final Map<String,Object> toSeal = new HashMap<>();
+
+ if (map.containsKey(TokenClaimsSet.KEY_TYPE)) {
+ toSeal.put(TokenClaimsSet.KEY_TYPE, map.remove(TokenClaimsSet.KEY_TYPE));
+ }
+
+ if (map.containsKey(TokenClaimsSet.KEY_USER_PRINCIPAL)) {
+ toSeal.put(TokenClaimsSet.KEY_USER_PRINCIPAL, map.remove(TokenClaimsSet.KEY_USER_PRINCIPAL));
+ }
+
+ if (map.containsKey(TokenClaimsSet.KEY_DELIVERY_CLAIMS)) {
+ toSeal.put(TokenClaimsSet.KEY_DELIVERY_CLAIMS, map.remove(TokenClaimsSet.KEY_DELIVERY_CLAIMS));
+ }
+
+ if (map.containsKey(TokenClaimsSet.KEY_DELIVERY_CLAIMS_USERINFO)) {
+ toSeal.put(TokenClaimsSet.KEY_DELIVERY_CLAIMS_USERINFO, map.remove(TokenClaimsSet.KEY_DELIVERY_CLAIMS_USERINFO));
+ }
+
+ if (map.containsKey(TokenClaimsSet.KEY_CONSENTED_CLAIMS)) {
+ toSeal.put(TokenClaimsSet.KEY_CONSENTED_CLAIMS, map.remove(TokenClaimsSet.KEY_CONSENTED_CLAIMS));
+ }
+
+ if (map.containsKey(TokenClaimsSet.KEY_CONSENT_ENABLED)) {
+ toSeal.put(TokenClaimsSet.KEY_CONSENT_ENABLED, map.remove(TokenClaimsSet.KEY_CONSENT_ENABLED));
+ }
+
+ if (map.containsKey(TokenClaimsSet.KEY_CODE_CHALLENGE)) {
+ toSeal.put(TokenClaimsSet.KEY_CODE_CHALLENGE, map.remove(TokenClaimsSet.KEY_CODE_CHALLENGE));
+ }
+
+ if (map.containsKey(TokenClaimsSet.KEY_NONCE)) {
+ toSeal.put(TokenClaimsSet.KEY_NONCE, map.remove(TokenClaimsSet.KEY_NONCE));
+ }
+
+ if (toSeal.isEmpty()) {
+ // Nothing to do.
+ return claims;
+ }
+
+ // Wrap the sealed claims and re-embed back in original claims set.
+ final String sealed = dataSealer.wrap(JWTClaimsSet.parse(toSeal).toString());
+ map.put(TokenClaimsSet.KEY_SEALED_FOR_OP, sealed);
+
+ // Re-parse the claims.
+ return JWTClaimsSet.parse(map);
+ }
+
}
\ 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/ValidateAudience.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/ValidateAudience.java
index 0b926b34..20ae0969 100644
--- a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/ValidateAudience.java
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/ValidateAudience.java
@@ -21,6 +21,7 @@ import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.function.Function;
+import java.util.function.Predicate;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
@@ -40,6 +41,7 @@ import net.shibboleth.idp.plugin.oidc.op.profile.context.navigate.ClientInfoAudi
import net.shibboleth.idp.plugin.oidc.op.profile.context.navigate.DefaultOIDCMetadataContextLookupFunction;
import net.shibboleth.idp.plugin.oidc.op.profile.context.navigate.TokenRequestAudienceLookupFunction;
import net.shibboleth.idp.plugin.oidc.op.profile.impl.AbstractOIDCAuthenticationResponseAction;
+import net.shibboleth.idp.plugin.oidc.op.profile.logic.IssueIDTokenCondition;
import net.shibboleth.idp.profile.context.navigate.RelyingPartyIdLookupFunction;
import net.shibboleth.oidc.profile.core.OidcEventIds;
import net.shibboleth.utilities.java.support.component.ComponentSupport;
@@ -53,8 +55,9 @@ import net.shibboleth.utilities.java.support.logic.Constraint;
* <p>This is an ordered list, so the first allowed value determines the "primary" audience of
* the eventual token.</p>
*
- * <p>Requesting values is optional, in the absence of which at least one allowed value must exist
- * and be returned, or a failure event will be signaled.</p>
+ * <p>Requesting values is optional. If the OP is an implied audience, then no other audience will
+ * be established, but if not then at least one audience must be permitted and the first permitted
+ * value will be assumed.</p>
*
* @event {@link EventIds#PROCEED_EVENT_ID}
* @event {@link EventIds#INVALID_PROFILE_CTX}
@@ -77,6 +80,9 @@ public class ValidateAudience extends AbstractOIDCAuthenticationResponseAction {
/** Strategy used for locating/creating the proxy context. */
@Nonnull private Function<ProfileRequestContext,ProxiedRequesterContext> proxiedRequesterContextCreationStrategy;
+ /** Whether the request includes the OP as an audience. */
+ @Nonnull private Predicate<ProfileRequestContext> selfAudienceCondition;
+
/** Strategy used to locate the {@link OIDCAuthenticationResponseTokenClaimsContext}. */
@Nonnull
private Function<ProfileRequestContext,OIDCAuthenticationResponseTokenClaimsContext>
@@ -90,6 +96,8 @@ public class ValidateAudience extends AbstractOIDCAuthenticationResponseAction {
new DefaultOIDCMetadataContextLookupFunction());
proxiedRequesterContextCreationStrategy = new ChildContextLookup<>(ProxiedRequesterContext.class, true).compose(
new OutboundMessageContextLookup());
+ // openid scope -> we're issuing an ID token -> the OP will be an audience for the access token
+ selfAudienceCondition = new IssueIDTokenCondition();
}
/**
@@ -126,7 +134,7 @@ public class ValidateAudience extends AbstractOIDCAuthenticationResponseAction {
ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
allowedAudienceLookupStrategy = Constraint.isNotNull(strategy,
- "Allowed scope lookyp strategy cannot be null");
+ "Allowed scope lookup strategy cannot be null");
}
/**
@@ -142,20 +150,29 @@ public class ValidateAudience extends AbstractOIDCAuthenticationResponseAction {
"ProxiedRequesterContext lookup strategy cannot be null");
}
+ /**
+ * Set whether the OP is an implied audience for the token request.
+ *
+ * @param condition condition to set
+ *
+ * @since 3.2.0
+ */
+ public void setSelfAudienceCondition(@Nonnull final Predicate<ProfileRequestContext> condition) {
+ ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+
+ selfAudienceCondition = Constraint.isNotNull(condition, "Self audience condition cannot be null");
+ }
+
// Checkstyle: CyclomaticComplexity OFF
/** {@inheritDoc} */
@Override
protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
final String clientId = relyingPartyIdLookupStrategy.apply(profileRequestContext);
+ final boolean allowNone = selfAudienceCondition.test(profileRequestContext);
// These may come from metadata or be supplemented or substituted from elsewhere.
final List<String> allowedAudience = allowedAudienceLookupStrategy.apply(profileRequestContext);
- if (allowedAudience == null || allowedAudience.isEmpty()) {
- log.warn("{} No allowed audience for client {}", getLogPrefix(), clientId);
- ActionSupport.buildEvent(profileRequestContext, OidcEventIds.INVALID_TARGET);
- return;
- }
// These come from a previous authorization grant (authz code or refresh token).
List<String> previouslyGrantedAudience = null;
@@ -166,7 +183,21 @@ public class ValidateAudience extends AbstractOIDCAuthenticationResponseAction {
// These come from a request object or parameter. Absent by definition on the UserInfo endpoint.
List<String> requestedAudience = requestedAudienceLookupStrategy != null ?
requestedAudienceLookupStrategy.apply(profileRequestContext) : null;
-
+
+ if (allowedAudience == null || allowedAudience.isEmpty()) {
+ if (allowNone) {
+ if (previouslyGrantedAudience != null || requestedAudience != null) {
+ log.warn("{} No allowed audiences for client {}, OP will be sole audience", getLogPrefix(), clientId);
+ } else {
+ log.debug("{} No allowed audiences for client {}, OP will be sole audience", getLogPrefix(), clientId);
+ }
+ } else {
+ log.warn("{} No allowed audience for client {}", getLogPrefix(), clientId);
+ ActionSupport.buildEvent(profileRequestContext, OidcEventIds.INVALID_TARGET);
+ }
+ return;
+ }
+
if (requestedAudience == null) {
// With none requested, simply swap requested for previously granted, if any.
// Set previous set to null since there's no need to filter against it.
@@ -175,7 +206,11 @@ public class ValidateAudience extends AbstractOIDCAuthenticationResponseAction {
}
if (requestedAudience == null) {
- // Nothing requested or previously granted, so take the first allowed value.
+ // Nothing requested or previously granted.
+ if (allowNone) {
+ log.debug("{} No audience in request for client {}, OP will be sole audience", getLogPrefix(), clientId);
+ return;
+ }
log.debug("{} No audience in request for client {}, using first allowed", getLogPrefix(), clientId);
requestedAudience = Collections.singletonList(allowedAudience.get(0));
}
@@ -195,8 +230,12 @@ public class ValidateAudience extends AbstractOIDCAuthenticationResponseAction {
}
if (effectiveAudience.isEmpty()) {
- log.warn("{} No allowed audience for client {}", getLogPrefix(), clientId);
- ActionSupport.buildEvent(profileRequestContext, OidcEventIds.INVALID_TARGET);
+ if (allowNone) {
+ log.debug("{} No allowed audience for client {}, OP will be sole audience", getLogPrefix(), clientId);
+ } else {
+ log.warn("{} No allowed audience for client {}", getLogPrefix(), clientId);
+ ActionSupport.buildEvent(profileRequestContext, OidcEventIds.INVALID_TARGET);
+ }
return;
}
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/AbstractOIDCTokenResponseAction.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/AbstractOIDCTokenResponseAction.java
index da1a579b..d66b8a9e 100644
--- a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/AbstractOIDCTokenResponseAction.java
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/AbstractOIDCTokenResponseAction.java
@@ -37,7 +37,7 @@ import net.shibboleth.oidc.metadata.context.OIDCMetadataContext;
* {@link ProfileRequestContext#getOutboundMessageContext()}. Extends base class that offers actions on
* {@link TokenRequest} found via {@link MessageContext#getMessage()}.
*/
-abstract class AbstractOIDCTokenResponseAction extends AbstractOIDCTokenRequestAction {
+public abstract class AbstractOIDCTokenResponseAction extends AbstractOIDCTokenRequestAction {
/** Class logger. */
@Nonnull private Logger log = LoggerFactory.getLogger(AbstractOIDCTokenResponseAction.class);
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/AddAccessTokenHashToIDToken.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/AddAccessTokenHashToIDToken.java
index 30602d48..fd3a2d39 100644
--- a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/AddAccessTokenHashToIDToken.java
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/AddAccessTokenHashToIDToken.java
@@ -52,7 +52,7 @@ public class AddAccessTokenHashToIDToken extends AbstractOIDCSigningResponseActi
return;
}
final AccessTokenHash atHash = AccessTokenHash.compute(getOidcResponseContext().getAccessToken(),
- new JWSAlgorithm(getSignatureSigningParameters().getSignatureAlgorithm()));
+ new JWSAlgorithm(getSignatureSigningParameters().getSignatureAlgorithm()), null);
if (atHash == null || atHash.getValue() == null) {
log.error("{} Not able to generate at_hash using algorithm {}", getLogPrefix(),
getSignatureSigningParameters().getSignatureAlgorithm());
@@ -63,7 +63,6 @@ public class AddAccessTokenHashToIDToken extends AbstractOIDCSigningResponseActi
getOidcResponseContext().getIDToken().setClaim(IDTokenClaimsSet.AT_HASH_CLAIM_NAME, atHash.getValue());
log.debug("{} Updated token {}", getLogPrefix(),
getOidcResponseContext().getIDToken().toJSONObject().toJSONString());
-
}
}
\ No newline at end of file
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/AddAttributesToClaimsSet.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/AddAttributesToClaimsSet.java
index 2c92c468..189eaf4a 100644
--- a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/AddAttributesToClaimsSet.java
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/AddAttributesToClaimsSet.java
@@ -119,7 +119,6 @@ public class AddAttributesToClaimsSet extends AbstractOIDCResponseAction {
/** List of claim names that will not be added. */
@Nullable @NonnullElements private List<String> reservedClaimNames;
-
/** Attributes to include in ID token no matter what. */
@Nullable @NonnullElements private Set<String> alwaysIncludedAttributes;
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/SetAccessTokenToResponseContext.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/SetAccessTokenToResponseContext.java
deleted file mode 100644
index befe031a..00000000
--- a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/SetAccessTokenToResponseContext.java
+++ /dev/null
@@ -1,332 +0,0 @@
-/*
- * Licensed to the University Corporation for Advanced Internet Development,
- * Inc. (UCAID) under one or more contributor license agreements. See the
- * NOTICE file distributed with this work for additional information regarding
- * copyright ownership. The UCAID licenses this file to You under the Apache
- * License, Version 2.0 (the "License"); you may not use this file except in
- * compliance with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package net.shibboleth.idp.plugin.oidc.op.profile.impl;
-
-import java.time.Duration;
-import java.time.Instant;
-import java.util.function.Function;
-import java.util.function.Predicate;
-
-import javax.annotation.Nonnull;
-import javax.annotation.Nullable;
-
-import org.opensaml.profile.action.EventIds;
-import org.opensaml.profile.context.ProfileRequestContext;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import com.nimbusds.oauth2.sdk.Scope;
-import com.nimbusds.openid.connect.sdk.AuthenticationRequest;
-import com.nimbusds.openid.connect.sdk.claims.ClaimsSet;
-
-import net.minidev.json.JSONArray;
-import net.shibboleth.idp.authn.context.SubjectContext;
-import net.shibboleth.idp.plugin.oidc.op.messaging.context.OIDCAuthenticationResponseConsentContext;
-import net.shibboleth.idp.plugin.oidc.op.messaging.context.OIDCAuthenticationResponseContext;
-import net.shibboleth.idp.plugin.oidc.op.messaging.context.OIDCAuthenticationResponseTokenClaimsContext;
-import net.shibboleth.idp.plugin.oidc.op.profile.context.navigate.OIDCAuthenticationResponseContextLookupFunction;
-import net.shibboleth.idp.plugin.oidc.op.token.support.AccessTokenClaimsSet;
-import net.shibboleth.idp.plugin.oidc.op.token.support.AuthorizeCodeClaimsSet;
-import net.shibboleth.idp.plugin.oidc.op.token.support.RefreshTokenClaimsSet;
-import net.shibboleth.idp.plugin.oidc.op.token.support.TokenClaimsSet;
-import net.shibboleth.idp.profile.IdPEventIds;
-import net.shibboleth.idp.profile.context.navigate.ResponderIdLookupFunction;
-import net.shibboleth.oidc.profile.config.logic.AttributeConsentFlowEnabledPredicate;
-import net.shibboleth.oidc.profile.config.navigate.AccessTokenLifetimeLookupFunction;
-
-import org.opensaml.messaging.context.navigate.ChildContextLookup;
-import org.opensaml.profile.action.ActionSupport;
-
-import net.shibboleth.utilities.java.support.annotation.constraint.NonnullAfterInit;
-import net.shibboleth.utilities.java.support.component.ComponentInitializationException;
-import net.shibboleth.utilities.java.support.component.ComponentSupport;
-import net.shibboleth.utilities.java.support.logic.Constraint;
-import net.shibboleth.utilities.java.support.logic.FunctionSupport;
-import net.shibboleth.utilities.java.support.security.DataSealer;
-import net.shibboleth.utilities.java.support.security.DataSealerException;
-import net.shibboleth.utilities.java.support.security.IdentifierGenerationStrategy;
-import net.shibboleth.utilities.java.support.security.impl.SecureRandomIdentifierGenerationStrategy;
-
-/**
- * Action that creates a Access Token, and sets it to work context
- * {@link OIDCAuthenticationResponseContext#getAccessToken()} located under
- * {@link ProfileRequestContext#getOutboundMessageContext()}.
- */
-public class SetAccessTokenToResponseContext extends AbstractOIDCResponseAction {
-
- /** Class logger. */
- @Nonnull private Logger log = LoggerFactory.getLogger(SetAccessTokenToResponseContext.class);
-
- /** Data sealer for handling access token. */
- @NonnullAfterInit private DataSealer dataSealer;
-
- /** Authorize Code / Refresh Token the access token is based on. */
- @Nullable private TokenClaimsSet tokenClaimsSet;
-
- /** Strategy used to obtain the response issuer value. */
- @Nonnull private Function<ProfileRequestContext, String> issuerLookupStrategy;
-
- /** Strategy used to obtain the access token lifetime. */
- @Nonnull private Function<ProfileRequestContext,Duration> accessTokenLifetimeLookupStrategy;
-
- /** Predicate used to check if consent is enabled with a given {@link ProfileRequestContext}. */
- @Nonnull
- private Predicate<ProfileRequestContext> consentEnabledPredicate;
-
- /** Access Token lifetime. */
- @Nullable private Duration accessTokenLifetime;
-
- /** Subject context. */
- @Nullable private SubjectContext subjectCtx;
-
- /** The generator to use. */
- @Nullable private IdentifierGenerationStrategy idGenerator;
-
- /** Strategy used to locate the {@link IdentifierGenerationStrategy} to use. */
- @Nonnull private Function<ProfileRequestContext, IdentifierGenerationStrategy> idGeneratorLookupStrategy;
-
- /** Authentication request the token is based on. */
- @Nullable private AuthenticationRequest authenticationRequest;
-
- /** Strategy used to locate the {@link OIDCAuthenticationResponseTokenClaimsContext}. */
- @Nonnull
- private Function<ProfileRequestContext,OIDCAuthenticationResponseTokenClaimsContext>
- tokenClaimsContextLookupStrategy;
-
- /** Strategy used to locate the {@link OIDCAuthenticationResponseConsentContext}. */
- @Nonnull
- private Function<ProfileRequestContext, OIDCAuthenticationResponseConsentContext> consentContextLookupStrategy;
-
- /**
- * Constructor.
- */
- public SetAccessTokenToResponseContext() {
- tokenClaimsContextLookupStrategy =
- new ChildContextLookup<>(OIDCAuthenticationResponseTokenClaimsContext.class).compose(
- new OIDCAuthenticationResponseContextLookupFunction());
- consentContextLookupStrategy =
- new ChildContextLookup<>(OIDCAuthenticationResponseConsentContext.class).compose(
- new OIDCAuthenticationResponseContextLookupFunction());
- accessTokenLifetimeLookupStrategy = new AccessTokenLifetimeLookupFunction();
- consentEnabledPredicate = new AttributeConsentFlowEnabledPredicate();
- issuerLookupStrategy = new ResponderIdLookupFunction();
- idGeneratorLookupStrategy = FunctionSupport.constant(new SecureRandomIdentifierGenerationStrategy());
- }
-
- /**
- * 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 strategy used to locate the {@link OIDCAuthenticationResponseTokenClaimsContext} associated with a given
- * {@link ProfileRequestContext}.
- *
- * @param strategy lookup strategy
- */
- public void setOIDCAuthenticationResponseTokenClaimsContextLookupStrategy(
- @Nonnull final Function<ProfileRequestContext, OIDCAuthenticationResponseTokenClaimsContext> strategy) {
- ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
- tokenClaimsContextLookupStrategy = Constraint.isNotNull(strategy,
- "OIDCAuthenticationResponseTokenClaimsContextt lookup strategy cannot be null");
- }
-
- /**
- * Set the strategy used to locate the {@link OIDCAuthenticationResponseConsentContext} associated with a given
- * {@link ProfileRequestContext}.
- *
- * @param strategy lookup strategy
- */
- public void setOIDCAuthenticationResponseConsentContextLookupStrategy(
- @Nonnull final Function<ProfileRequestContext, OIDCAuthenticationResponseConsentContext> strategy) {
- ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
- consentContextLookupStrategy = Constraint.isNotNull(strategy,
- "OIDCAuthenticationResponseConsentContext lookup strategy cannot be null");
- }
-
- /**
- * Set the strategy used to obtain the access token lifetime.
- *
- * @param strategy lookup strategy
- */
- public void setAccessTokenLifetimeLookupStrategy(@Nonnull final Function<ProfileRequestContext,Duration> strategy) {
- ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
-
- accessTokenLifetimeLookupStrategy =
- Constraint.isNotNull(strategy, "Access token lifetime lookup strategy cannot be null");
- }
-
- /**
- * Set the strategy used to locate the {@link IdentifierGenerationStrategy} to use.
- *
- * @param strategy lookup strategy
- */
- public void setIdentifierGeneratorLookupStrategy(
- @Nonnull final Function<ProfileRequestContext, IdentifierGenerationStrategy> strategy) {
- ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
-
- idGeneratorLookupStrategy =
- Constraint.isNotNull(strategy, "Identifier generation strategy cannot be null");
- }
-
- /**
- * Set the strategy used to locate the issuer value to use.
- *
- * @param strategy lookup strategy
- */
- public void setIssuerLookupStrategy(@Nonnull final Function<ProfileRequestContext, String> strategy) {
- ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
-
- issuerLookupStrategy = Constraint.isNotNull(strategy, "Issuer lookup strategy cannot be null");
- }
-
- /**
- * Set the predicate used to check if consent is enabled with a given {@link ProfileRequestContext}.
- *
- * @param predicate predicate used to check if consent is enabled with a given {@link ProfileRequestContext}.
- */
- public void setConsentEnabledPredicate(@Nonnull final Predicate<ProfileRequestContext> predicate) {
- ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
-
- consentEnabledPredicate =
- Constraint.isNotNull(predicate, "predicate used to check if consent is enabled cannot be null");
- }
-
- /** {@inheritDoc} */
- @Override
- protected void doInitialize() throws ComponentInitializationException {
- super.doInitialize();
-
- if (dataSealer == null) {
- throw new ComponentInitializationException("DataSealer cannot be null");
- }
- }
-
- // Checkstyle: CyclomaticComplexity OFF
- /** {@inheritDoc} */
- @Override
- protected boolean doPreExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
- if (!super.doPreExecute(profileRequestContext)) {
- return false;
- }
-
- accessTokenLifetime = accessTokenLifetimeLookupStrategy.apply(profileRequestContext);
- if (accessTokenLifetime == null) {
- log.warn("{} No lifetime supplied for access token", getLogPrefix());
- ActionSupport.buildEvent(profileRequestContext, IdPEventIds.INVALID_PROFILE_CONFIG);
- return false;
- }
-
- tokenClaimsSet = getOidcResponseContext().getAuthorizationGrantClaimsSet();
- if (tokenClaimsSet != null && !(tokenClaimsSet instanceof RefreshTokenClaimsSet)
- && !(tokenClaimsSet instanceof AuthorizeCodeClaimsSet)) {
- log.error("{} No token grant if of illegal type", getLogPrefix());
- ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
- return false;
- } else if (tokenClaimsSet == null) {
- /*
- * Alternate path possible only when access token is to be provided by authz endpoint without authorization
- * code This is the case only with "token id_token" response type. Unusually complex initialization.
- */
- subjectCtx = profileRequestContext.getSubcontext(SubjectContext.class, false);
- if (subjectCtx == null) {
- log.error("{} No subject context", getLogPrefix());
- ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
- return false;
- }
- idGenerator = idGeneratorLookupStrategy.apply(profileRequestContext);
- if (idGenerator == null) {
- log.error("{} No identifier generation strategy", getLogPrefix());
- ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
- return false;
- }
- if (profileRequestContext.getInboundMessageContext() == null
- || profileRequestContext.getInboundMessageContext().getMessage() == null || !(profileRequestContext
- .getInboundMessageContext().getMessage() instanceof AuthenticationRequest)) {
- log.error("{} No authentication request avalailable", getLogPrefix());
- ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_MSG_CTX);
- return false;
- }
- authenticationRequest =
- (AuthenticationRequest) profileRequestContext.getInboundMessageContext().getMessage();
- }
- return true;
- }
- // Checkstyle: CyclomaticComplexity ON
-
- /** {@inheritDoc} */
- @Override
- protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
- final Instant dateExp = Instant.now().plus(accessTokenLifetime);
- ClaimsSet claims = null;
- ClaimsSet claimsUI = null;
- final OIDCAuthenticationResponseTokenClaimsContext tokenClaimsCtx =
- tokenClaimsContextLookupStrategy.apply(profileRequestContext);
- if (tokenClaimsCtx != null) {
- claims = tokenClaimsCtx.getClaims();
- claimsUI = tokenClaimsCtx.getUserinfoClaims();
- }
- final AccessTokenClaimsSet claimsSet;
- if (tokenClaimsSet != null) {
- // We may not use original claims as input for scope / delivery claims as they may have been reduced.
- claimsSet = new AccessTokenClaimsSet.Builder(tokenClaimsSet,
- getOidcResponseContext().getScope() != null ? getOidcResponseContext().getScope() : new Scope(),
- claims, claimsUI, Instant.now(), dateExp).build();
- } else {
- final OIDCAuthenticationResponseConsentContext consentCtx =
- consentContextLookupStrategy.apply(profileRequestContext);
- final JSONArray consented = consentCtx != null ? consentCtx.getConsentedAttributes() : null;
- // "token id_token" response type. Access token is not derived from Authorization code / Refresh token..
- claimsSet = new AccessTokenClaimsSet.Builder()
- .setJWTID(idGenerator)
- .setClientID(authenticationRequest.getClientID())
- .setIssuer(issuerLookupStrategy.apply(profileRequestContext))
- .setPrincipal(subjectCtx.getPrincipalName())
- .setSubject(getOidcResponseContext().getSubject())
- .setIssuedAt(Instant.now())
- .setExpiresAt(dateExp)
- .setAuthenticationTime(getOidcResponseContext().getAuthTime())
- .setRedirectURI(getOidcResponseContext().getRedirectURI())
- .setScope(getOidcResponseContext().getScope())
- .setACR(getOidcResponseContext().getAcr())
- .setNonce(authenticationRequest.getNonce())
- .setClaimsRequest(authenticationRequest.getOIDCClaims())
- .setDlClaims(claims)
- .setDlClaimsUI(claimsUI)
- .setConsentedClaims(consented)
- .setConsentEnabled(consentEnabledPredicate.test(profileRequestContext))
- .build();
- }
-
- try {
- getOidcResponseContext().setAccessToken(claimsSet.serialize(dataSealer), accessTokenLifetime);
- log.debug("{} Setting access token {} as {} to response context ", getLogPrefix(), claimsSet.serialize(),
- getOidcResponseContext().getAccessToken());
- } catch (final DataSealerException e) {
- log.error("{} Access Token generation failed {}", getLogPrefix(), e.getMessage());
- ActionSupport.buildEvent(profileRequestContext, EventIds.UNABLE_TO_ENCRYPT);
- }
-
- }
-
-}
\ No newline at end of file
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/SetAuthorizationCodeToResponseContext.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/SetAuthorizationCodeToResponseContext.java
index a1033fac..9b784a4a 100644
--- a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/SetAuthorizationCodeToResponseContext.java
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/SetAuthorizationCodeToResponseContext.java
@@ -289,6 +289,8 @@ public class SetAuthorizationCodeToResponseContext extends AbstractOIDCAuthentic
@Override
protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
+ final OIDCAuthenticationResponseContext responseCtx = getOidcResponseContext();
+
final OIDCAuthenticationResponseConsentContext consentCtx =
consentContextLookupStrategy.apply(profileRequestContext);
final JSONArray consented = consentCtx != null ? consentCtx.getConsentedAttributes() : null;
@@ -303,22 +305,23 @@ public class SetAuthorizationCodeToResponseContext extends AbstractOIDCAuthentic
claimsUI = tokenClaimsCtx.getUserinfoClaims();
}
final Instant dateExp = Instant.now().plus(authzCodeLifetime);
- final Scope scope = getOidcResponseContext().getScope();
+ final Scope scope = responseCtx.getScope();
final AuthorizeCodeClaimsSet claimsSet = new AuthorizeCodeClaimsSet.Builder()
.setJWTID(idGenerator)
.setClientID(getAuthenticationRequest().getClientID())
.setIssuer(issuerLookupStrategy.apply(profileRequestContext))
.setPrincipal(subjectCtx.getPrincipalName())
- .setSubject(getOidcResponseContext().getSubject())
+ .setSubject(responseCtx.getSubject())
.setIssuedAt(Instant.now())
.setExpiresAt(dateExp)
- .setAuthenticationTime(getOidcResponseContext().getAuthTime())
- .setRedirectURI(getOidcResponseContext().getRedirectURI())
+ .setAuthenticationTime(responseCtx.getAuthTime())
+ .setRedirectURI(responseCtx.getRedirectURI())
.setScope(scope != null ? scope : new Scope())
- .setACR(getOidcResponseContext().getAcr())
+ .setAudience(responseCtx.getAudience())
+ .setACR(responseCtx.getAcr())
.setNonce(new DefaultRequestNonceLookupFunction().apply(profileRequestContext))
.setCodeChallenge(codeChallenge)
- .setClaimsRequest(getOidcResponseContext().getRequestedClaims())
+ .setClaimsRequest(responseCtx.getRequestedClaims())
.setDlClaims(claims)
.setDlClaimsID(claimsID)
.setDlClaimsUI(claimsUI)
@@ -326,11 +329,11 @@ public class SetAuthorizationCodeToResponseContext extends AbstractOIDCAuthentic
.setConsentEnabled(consentEnabledPredicate.test(profileRequestContext))
.build();
// We set token claims set to response context for possible access token generation.
- getOidcResponseContext().setAuthorizationGrantClaimsSet(claimsSet);
+ responseCtx.setAuthorizationGrantClaimsSet(claimsSet);
try {
- getOidcResponseContext().setAuthorizationCode(claimsSet.serialize(dataSealer));
+ responseCtx.setAuthorizationCode(claimsSet.serialize(dataSealer));
log.debug("{} Setting authz code {} as {} to response context ", getLogPrefix(), claimsSet.serialize(),
- getOidcResponseContext().getAuthorizationCode());
+ responseCtx.getAuthorizationCode());
} catch (final DataSealerException e) {
log.error("{} Authorization Code generation failed {}", getLogPrefix(), e.getMessage());
ActionSupport.buildEvent(profileRequestContext, EventIds.UNABLE_TO_ENCRYPT);
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/SetTokenDeliveryAttributesFromTokenToResponseContext.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/SetTokenDeliveryAttributesFromTokenToResponseContext.java
index 048afbc8..7ff1d0c4 100644
--- a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/SetTokenDeliveryAttributesFromTokenToResponseContext.java
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/SetTokenDeliveryAttributesFromTokenToResponseContext.java
@@ -39,7 +39,7 @@ import com.nimbusds.openid.connect.sdk.claims.ClaimsSet;
/**
* Action that locates any token delivery claims from authorization code / access token. For located claims
* {@link OIDCAuthenticationResponseTokenClaimsContext} is created under {@link OIDCAuthenticationResponseContext} and
- * the claims are placed there. Token and user info end points use the context for forming response.
+ * the claims are placed there. Token and user info endpoints use the context for forming response.
**/
public class SetTokenDeliveryAttributesFromTokenToResponseContext extends AbstractOIDCResponseAction {
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 6a8920e4..4596acdd 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
@@ -198,14 +198,14 @@ public class ValidateGrant extends AbstractOIDCTokenResponseAction {
log.error("{} Replay detected of authz code {}", getLogPrefix(), authzCodeClaimsSet.getID());
if (!revocationCache.revoke(RevocationCacheContexts.AUTHORIZATION_CODE,
authzCodeClaimsSet.getID())) {
- log.warn("{} Fatal error, unable to set entry to revocation cache", getLogPrefix());
+ log.warn("{} Fatal error, unable to save replayed code to revocation cache", getLogPrefix());
}
ActionSupport.buildEvent(profileRequestContext, OidcEventIds.INVALID_GRANT);
return;
}
tokenClaimsSet = authzCodeClaimsSet;
} catch (final DataSealerException | ParseException e) {
- log.warn("{} Obtaining authz code failed {}", getLogPrefix(), e.getMessage());
+ log.warn("{} Unwrapping authz code failed: {}", getLogPrefix(), e.getMessage());
ActionSupport.buildEvent(profileRequestContext, OidcEventIds.INVALID_GRANT);
return;
}
@@ -231,7 +231,7 @@ public class ValidateGrant extends AbstractOIDCTokenResponseAction {
}
tokenClaimsSet = refreshTokenClaimsSet;
} catch (final ParseException | DataSealerException e) {
- log.warn("{} Obtaining refresh token failed {}", getLogPrefix(), e.getMessage());
+ log.warn("{} Unwrapping refresh token failed {}", getLogPrefix(), e.getMessage());
ActionSupport.buildEvent(profileRequestContext, OidcEventIds.INVALID_GRANT);
return;
}
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/ValidateRedirectURI.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/ValidateRedirectURI.java
index 7e7029ff..36d9c1fc 100644
--- a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/ValidateRedirectURI.java
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/ValidateRedirectURI.java
@@ -35,7 +35,7 @@ import net.shibboleth.utilities.java.support.component.ComponentSupport;
import net.shibboleth.utilities.java.support.logic.Constraint;
/**
- * Action that validates redirect uri is a expected one. Validated redirect uri is stored to response context.
+ * Action that validates redirect uri is expected. Validated redirect uri is stored to response context.
*/
public class ValidateRedirectURI extends AbstractOIDCAuthenticationResponseAction {
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/ValidateScope.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/ValidateScope.java
index ae30bf7a..1b801ea1 100644
--- a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/ValidateScope.java
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/ValidateScope.java
@@ -49,7 +49,7 @@ import net.shibboleth.utilities.java.support.logic.Constraint;
* <p>Requested scopes come from the inbound message and are possible but optional for both
* authorization and token requests. They come from lookup functions aware of each message type.</p>
*
- * <p>Previously granted scopes are stored in the response context's slow for previous authorization
+ * <p>Previously granted scopes are stored in the response context's slot for previous authorization
* grant claims. In the case where no scopes are explicitly requested, we still filter the previous
* grants against the metadata.</p>
*
@@ -155,7 +155,7 @@ public class ValidateScope extends AbstractOIDCAuthenticationResponseAction {
return;
}
- // These come from a previous authorization grant (authz code or refresh token).
+ // These come from a previous authorization grant (authz code or access/refresh token).
Scope previouslyGrantedScopes = null;
if (getOidcResponseContext().getAuthorizationGrantClaimsSet() != null) {
previouslyGrantedScopes = getOidcResponseContext().getAuthorizationGrantClaimsSet().getScope();
@@ -188,7 +188,7 @@ public class ValidateScope extends AbstractOIDCAuthenticationResponseAction {
}
if (requestedScopes.contains(OIDCScopeValue.OFFLINE_ACCESS)) {
- // DefaultRequestResponseTypeLookupFunction returns response type only in authentication end point.
+ // DefaultRequestResponseTypeLookupFunction returns response type only on authorization end point.
// It is enough to remove offline_scope in this first validation turn.
final ResponseType responseType =
new DefaultRequestResponseTypeLookupFunction().apply(profileRequestContext);
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/logic/AttributeConsentEnabledInTokenClaimsSetPredicate.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/logic/AttributeConsentEnabledInTokenClaimsSetPredicate.java
index 9b8172b6..a58cc9cf 100644
--- a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/logic/AttributeConsentEnabledInTokenClaimsSetPredicate.java
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/logic/AttributeConsentEnabledInTokenClaimsSetPredicate.java
@@ -39,7 +39,7 @@ public class AttributeConsentEnabledInTokenClaimsSetPredicate extends AbstractRe
final MessageContext outboundMessageCtx = input.getOutboundMessageContext();
if (outboundMessageCtx != null) {
final OIDCAuthenticationResponseContext oidcResponseContext =
- outboundMessageCtx.getSubcontext(OIDCAuthenticationResponseContext.class, false);
+ outboundMessageCtx.getSubcontext(OIDCAuthenticationResponseContext.class);
if (oidcResponseContext != null && oidcResponseContext.getAuthorizationGrantClaimsSet() != null) {
return oidcResponseContext.getAuthorizationGrantClaimsSet().isConsentEnabled();
}
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/BuildUserInfoErrorResponseFromEvent.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/userinfo/profile/impl/BuildUserInfoErrorResponseFromEvent.java
similarity index 91%
rename from idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/BuildUserInfoErrorResponseFromEvent.java
rename to idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/userinfo/profile/impl/BuildUserInfoErrorResponseFromEvent.java
index ac492442..f1e50494 100644
--- a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/BuildUserInfoErrorResponseFromEvent.java
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/userinfo/profile/impl/BuildUserInfoErrorResponseFromEvent.java
@@ -15,7 +15,7 @@
* limitations under the License.
*/
-package net.shibboleth.idp.plugin.oidc.op.profile.impl;
+package net.shibboleth.idp.plugin.oidc.op.userinfo.profile.impl;
import org.opensaml.profile.context.EventContext;
import org.opensaml.profile.context.ProfileRequestContext;
@@ -23,6 +23,8 @@ import org.opensaml.profile.context.ProfileRequestContext;
import com.nimbusds.oauth2.sdk.ErrorObject;
import com.nimbusds.openid.connect.sdk.UserInfoErrorResponse;
+import net.shibboleth.idp.plugin.oidc.op.profile.impl.AbstractBuildErrorResponseFromEvent;
+
/**
* This action reads an event from the configured {@link EventContext} lookup strategy, constructs an OIDC user info
* error response message and attaches it as the outbound message.
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/FormOutboundUserInfoResponseMessage.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/userinfo/profile/impl/FormOutboundUserInfoResponseMessage.java
similarity index 94%
rename from idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/FormOutboundUserInfoResponseMessage.java
rename to idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/userinfo/profile/impl/FormOutboundUserInfoResponseMessage.java
index 541ea765..ceef2a1a 100644
--- a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/FormOutboundUserInfoResponseMessage.java
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/userinfo/profile/impl/FormOutboundUserInfoResponseMessage.java
@@ -15,7 +15,7 @@
* limitations under the License.
*/
-package net.shibboleth.idp.plugin.oidc.op.profile.impl;
+package net.shibboleth.idp.plugin.oidc.op.userinfo.profile.impl;
import javax.annotation.Nonnull;
@@ -27,6 +27,7 @@ import org.slf4j.LoggerFactory;
import com.nimbusds.openid.connect.sdk.UserInfoSuccessResponse;
import net.shibboleth.idp.plugin.oidc.op.messaging.context.OIDCAuthenticationResponseContext;
+import net.shibboleth.idp.plugin.oidc.op.profile.impl.AbstractOIDCTokenResponseAction;
/**
* Action that forms outbound message based on response context. Formed message is set to
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/InitializeOutboundUserInfoResponseMessageContext.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/userinfo/profile/impl/InitializeOutboundUserInfoResponseMessageContext.java
similarity index 88%
rename from idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/InitializeOutboundUserInfoResponseMessageContext.java
rename to idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/userinfo/profile/impl/InitializeOutboundUserInfoResponseMessageContext.java
index e625c01d..2af7a926 100644
--- a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/InitializeOutboundUserInfoResponseMessageContext.java
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/userinfo/profile/impl/InitializeOutboundUserInfoResponseMessageContext.java
@@ -15,11 +15,13 @@
* limitations under the License.
*/
-package net.shibboleth.idp.plugin.oidc.op.profile.impl;
+package net.shibboleth.idp.plugin.oidc.op.userinfo.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.
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/userinfo/profile/impl/ParseAccessToken.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/userinfo/profile/impl/ParseAccessToken.java
new file mode 100644
index 00000000..54989a7f
--- /dev/null
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/userinfo/profile/impl/ParseAccessToken.java
@@ -0,0 +1,203 @@
+/*
+ * 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.userinfo.profile.impl;
+
+import java.text.ParseException;
+import java.util.ArrayList;
+import java.util.Collection;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.profile.action.ActionSupport;
+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.jose.JOSEObjectType;
+import com.nimbusds.jwt.SignedJWT;
+import com.nimbusds.oauth2.sdk.token.AccessToken;
+
+import net.shibboleth.idp.plugin.oidc.op.messaging.context.OIDCAuthenticationResponseContext;
+import net.shibboleth.idp.plugin.oidc.op.token.support.AccessTokenClaimsSet;
+import net.shibboleth.oidc.profile.core.OidcEventIds;
+import net.shibboleth.oidc.security.impl.JWTSignatureValidationUtil;
+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.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 parses an access token and initially populates the claims for later
+ * validation.
+ *
+ * <p>Signed JWTs are also signature-checked here.</p>
+ *
+ * <p>The parsed token is stored to the response context retrievable as claims via
+ * {@link OIDCAuthenticationResponseContext#getTokenClaimsSet()}. Claims validation takes
+ * place later in order to allow for metadata and relying-party/profile config
+ * lookup to allow for pluggable validation, an overridden OP/issuer name, etc.</p>
+ *
+ * @event {@link EventIds#PROCEED_EVENT_ID}
+ * @event {@link OidcEventIds#INVALID_GRANT}
+ *
+ * @since 3.2.0
+ */
+public class ParseAccessToken extends AbstractOIDCUserInfoValidationResponseAction {
+
+ /** Class logger. */
+ @Nonnull private Logger log = LoggerFactory.getLogger(ParseAccessToken.class);
+
+ /** Data sealer for unwrapping authorization code. */
+ @NonnullAfterInit private DataSealer dataSealer;
+
+ /** Source of signing keys. */
+ @Nullable private CredentialResolver credentialResolver;
+
+ /** Copy of signed JWT for non-opaque access tokens. */
+ @Nullable private SignedJWT signedJWT;
+
+ /**
+ * Set the data sealer instance to use.
+ *
+ * @param sealer sealer to use
+ */
+ public void setDataSealer(@Nonnull final DataSealer sealer) {
+ ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+ dataSealer = Constraint.isNotNull(sealer, "DataSealer 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 void doInitialize() throws ComponentInitializationException {
+ super.doInitialize();
+
+ if (dataSealer == null) {
+ throw new ComponentInitializationException("DataSealer cannot be null");
+ }
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
+
+ final AccessToken token = getUserInfoRequest().getAccessToken();
+ if (token == null) {
+ log.error("{} Token missing from request", getLogPrefix());
+ ActionSupport.buildEvent(profileRequestContext, OidcEventIds.INVALID_GRANT);
+ return;
+ }
+
+ final AccessTokenClaimsSet accessTokenClaimsSet = parseAccessToken(token);
+ if (accessTokenClaimsSet == null) {
+ log.warn("{} Unable to parse/decode token for validation", getLogPrefix());
+ ActionSupport.buildEvent(profileRequestContext, OidcEventIds.INVALID_GRANT);
+ return;
+ }
+
+ log.debug("{} Access token unwrapped: {}", getLogPrefix(), accessTokenClaimsSet.serialize());
+
+ if (signedJWT != null) {
+ // Check typ header.
+ final JOSEObjectType typ = signedJWT.getHeader().getType();
+ if (typ == null || !"at+jwt".equals(typ.getType())) {
+ log.warn("{} Missing or invalid token type: {}", getLogPrefix(), typ != null ? typ.getType() : "null");
+ ActionSupport.buildEvent(profileRequestContext, OidcEventIds.INVALID_GRANT);
+ return;
+ }
+
+ if (credentialResolver == null) {
+ log.error("{} No CredentialResolver available, can't verify JWT signature", getLogPrefix());
+ ActionSupport.buildEvent(profileRequestContext, OidcEventIds.INVALID_GRANT);
+ 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);
+ ActionSupport.buildEvent(profileRequestContext, OidcEventIds.INVALID_GRANT);
+ return;
+ }
+ final String errorEventId = JWTSignatureValidationUtil.validateSignatureEx(credList, signedJWT,
+ OidcEventIds.INVALID_GRANT);
+ if (errorEventId != null) {
+ log.warn("{} Signature on token ID '{}' invalid", getLogPrefix(), accessTokenClaimsSet.getID());
+ ActionSupport.buildEvent(profileRequestContext, errorEventId);
+ return;
+ }
+ }
+
+ log.debug("{} Access token {} parsed", getLogPrefix(), accessTokenClaimsSet.getID());
+ getOidcResponseContext().setAuthorizationGrantClaimsSet(accessTokenClaimsSet);
+ }
+
+ /**
+ * Attempt to parse token.
+ *
+ * @param token the token
+ *
+ * @return parsed claim set or null
+ */
+ @Nullable protected AccessTokenClaimsSet parseAccessToken(@Nonnull @NotEmpty final AccessToken token) {
+
+ // Try parsing as a JWT.
+ try {
+ signedJWT = SignedJWT.parse(token.getValue());
+ return AccessTokenClaimsSet.parse(signedJWT, dataSealer);
+ } catch (final DataSealerException | ParseException e) {
+
+ }
+
+ // Fall back to opaque.
+ try {
+ return AccessTokenClaimsSet.parse(token.getValue(), dataSealer);
+ } 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/profile/impl/SignUserInfoResponse.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/userinfo/profile/impl/SignUserInfoResponse.java
similarity index 96%
rename from idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/SignUserInfoResponse.java
rename to idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/userinfo/profile/impl/SignUserInfoResponse.java
index f974961f..50652c2d 100644
--- a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/SignUserInfoResponse.java
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/userinfo/profile/impl/SignUserInfoResponse.java
@@ -15,7 +15,7 @@
* limitations under the License.
*/
-package net.shibboleth.idp.plugin.oidc.op.profile.impl;
+package net.shibboleth.idp.plugin.oidc.op.userinfo.profile.impl;
import java.util.function.Function;
@@ -36,6 +36,7 @@ import com.nimbusds.openid.connect.sdk.claims.UserInfo;
import net.shibboleth.idp.plugin.oidc.op.messaging.context.OIDCAuthenticationResponseContext;
import net.shibboleth.idp.plugin.oidc.op.profile.context.navigate.DefaultUserInfoSigningAlgLookupFunction;
+import net.shibboleth.idp.plugin.oidc.op.profile.impl.AbstractSignJWTAction;
import net.shibboleth.utilities.java.support.component.ComponentSupport;
import net.shibboleth.utilities.java.support.logic.Constraint;
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 c0e2fe9f..2941b8f1 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
@@ -17,109 +17,105 @@
package net.shibboleth.idp.plugin.oidc.op.userinfo.profile.impl;
-import java.text.ParseException;
+import java.util.function.Function;
import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
import org.opensaml.profile.action.ActionSupport;
+import org.opensaml.profile.action.EventIds;
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.storage.RevocationCacheContexts;
import net.shibboleth.idp.plugin.oidc.op.token.support.AccessTokenClaimsSet;
+import net.shibboleth.idp.plugin.oidc.op.token.support.TokenClaimsSet;
+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.utilities.java.support.annotation.constraint.NonnullAfterInit;
-import net.shibboleth.utilities.java.support.component.ComponentInitializationException;
import net.shibboleth.utilities.java.support.component.ComponentSupport;
import net.shibboleth.utilities.java.support.logic.Constraint;
-import net.shibboleth.utilities.java.support.security.DataSealer;
-import net.shibboleth.utilities.java.support.security.DataSealerException;
/**
- * Action that validates access token is a valid one. Token is valid if it is successfully unwrapped, parsed as access
- * token, is not expired and authorize code it has been derived from has not been revoked. Validated token is stored to
- * response context retrievable as claims {@link OIDCAuthenticationResponseContext#getTokenClaimsSet()}.
+ * Action that validates the claims pulled from an access token as usable for access
+ * to the OP's UserInfo endpoint.
+ *
+ * <p>The parsed claims are pulled from
+ * {@link OIDCAuthenticationResponseContext#getAuthorizationGrantClaimsSet()}.</p>
+ *
+ * @event {@link EventIds#PROCEED_EVENT_ID}
+ * @event {@link IdPEventIds#INVALID_PROFILE_CONFIG}
+ * @event {@link OidcEventIds#INVALID_GRANT}
*/
public class ValidateAccessToken extends AbstractOIDCUserInfoValidationResponseAction {
/** Class logger. */
@Nonnull private Logger log = LoggerFactory.getLogger(ValidateAccessToken.class);
-
- /** Data sealer for unwrapping authorization code. */
- @NonnullAfterInit private DataSealer dataSealer;
-
- /** Message revocation cache instance to use. */
- @NonnullAfterInit private RevocationCache revocationCache;
- /**
- * Set the data sealer instance to use.
- *
- * @param sealer sealer to use
- */
- public void setDataSealer(@Nonnull final DataSealer sealer) {
- ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
- dataSealer = Constraint.isNotNull(sealer, "DataSealer cannot be null");
- }
+ /** Lookup strategy for claims validator. */
+ @Nonnull private Function<ProfileRequestContext,ClaimsValidator> claimsValidatorLookupStrategy;
+ /** The claims validator to use. */
+ @Nullable private ClaimsValidator claimsValidator;
+
+ /** Constructor. */
+ public ValidateAccessToken() {
+ claimsValidatorLookupStrategy = new IssuedClaimsValidatorLookupFunction();
+ }
+
/**
- * Set the revocation cache instance to use.
+ * Set the claims validator lookup strategy.
*
- * @param cache revocation cache to set
+ * @param strategy lookup strategy
*/
- public void setRevocationCache(@Nonnull final RevocationCache cache) {
+ public void setClaimsValidatorLookupStrategy(
+ @Nonnull final Function<ProfileRequestContext,ClaimsValidator> strategy) {
ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
- revocationCache = Constraint.isNotNull(cache, "RevocationCache cannot be null");
+ claimsValidatorLookupStrategy = Constraint.isNotNull(strategy, "Lookup strategy cannot be null");
}
-
+
/** {@inheritDoc} */
@Override
- protected void doInitialize() throws ComponentInitializationException {
- super.doInitialize();
+ protected boolean doPreExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
+ if (!super.doPreExecute(profileRequestContext)) {
+ return false;
+ }
- if (revocationCache == null || dataSealer == null) {
- throw new ComponentInitializationException("RevocationCache and DataSealer cannot be null");
+ 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) {
-
- final AccessTokenClaimsSet accessTokenClaimsSet;
- try {
- accessTokenClaimsSet =
- AccessTokenClaimsSet.parse(getUserInfoRequest().getAccessToken().getValue(), dataSealer);
- log.debug("{} Access token unwrapped: {}", getLogPrefix(), accessTokenClaimsSet.serialize());
- } catch (final DataSealerException | ParseException e) {
- log.warn("{} Parsing access token failed: {}", getLogPrefix(), e.getMessage());
- ActionSupport.buildEvent(profileRequestContext, OidcEventIds.INVALID_GRANT);
- return;
- }
- if (!accessTokenClaimsSet.isTimeValid()) {
- log.warn("{} Access token is expired or future dated", getLogPrefix());
+ final TokenClaimsSet tokenClaims = getOidcResponseContext().getAuthorizationGrantClaimsSet();
+ if (!(tokenClaims instanceof AccessTokenClaimsSet) || tokenClaims.getClaimsSet() == null) {
+ log.error("{} Claims validation failed, unable to locate access token claims set to validate", getLogPrefix());
ActionSupport.buildEvent(profileRequestContext, OidcEventIds.INVALID_GRANT);
return;
}
-
- if (!accessTokenClaimsSet.getAudience().isEmpty()) {
- log.warn("{} Access token was not issued for use by this OP");
- ActionSupport.buildEvent(profileRequestContext, OidcEventIds.INVALID_GRANT);
- return;
- }
-
- if (revocationCache.isRevoked(RevocationCacheContexts.AUTHORIZATION_CODE, accessTokenClaimsSet.getID())) {
- log.warn("{} Authorization code {} and all derived tokens have been revoked", getLogPrefix(),
- accessTokenClaimsSet.getID());
+
+ log.debug("{} Validating parsed/decoded claims set: {}", getLogPrefix(), tokenClaims.getClaimsSet().toString());
+ try {
+ claimsValidator.validate(tokenClaims.getClaimsSet(), profileRequestContext);
+ } catch (final JWTValidationException e) {
+ log.warn("{} Claims validation failed, token is invalid: {}", getLogPrefix(), e.getMessage());
ActionSupport.buildEvent(profileRequestContext, OidcEventIds.INVALID_GRANT);
return;
}
-
- log.debug("{} Access token {} validated", getLogPrefix(), accessTokenClaimsSet.getID());
- getOidcResponseContext().setAuthorizationGrantClaimsSet(accessTokenClaimsSet);
+
+ log.debug("{} Access token {} validated", getLogPrefix(), tokenClaims.getID());
}
}
\ No newline at end of file
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 394f3440..ce0505dc 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
@@ -157,6 +157,7 @@
<value>acr</value>
<value>amr</value>
<value>auth_time</value>
+ <value>for_op</value>
</list>
</property>
</bean>
diff --git a/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/authorize/authorize-beans.xml b/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/authorize/authorize-beans.xml
index 57d27858..61033f4a 100644
--- a/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/authorize/authorize-beans.xml
+++ b/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/authorize/authorize-beans.xml
@@ -45,9 +45,6 @@
class="net.shibboleth.idp.plugin.oidc.op.profile.impl.SetRequestedClaimsToResponseContext" scope="prototype"
p:transcoderRegistry-ref="shibboleth.AttributeRegistryService" />
- <bean id="VerifyRequestedSubjectIdentifier"
- class="net.shibboleth.idp.plugin.oidc.op.profile.impl.VerifyRequestedSubjectIdentifier" scope="prototype" />
-
<bean id="PopulatePostAuthnInterceptContext"
class="net.shibboleth.idp.profile.interceptor.impl.PopulateProfileInterceptorContext" scope="prototype"
p:availableFlows="#{@'shibboleth.ProfileInterceptorFlowDescriptorManager'.getComponents()}"
@@ -182,34 +179,13 @@
<bean id="ValidateScope" class="net.shibboleth.idp.plugin.oidc.op.profile.impl.ValidateScope" scope="prototype"
p:allowedScopeLookupStrategy="#{getObject('shibboleth.oidc.AllowedScopeStrategy') ?: getObject('shibboleth.oidc.DefaultAllowedScopeStrategy')}" />
- <bean id="PopulateIDTokenSignatureSigningParameters"
- class="net.shibboleth.idp.plugin.oidc.op.profile.impl.PopulateOIDCSignatureSigningParameters " scope="prototype"
- c:strategy-ref="shibboleth.MessageContextLookup.Inbound"
- p:configurationLookupStrategy-ref="shibboleth.SignatureSigningConfigurationLookup"
- p:signatureSigningParametersResolver-ref="shibboleth.oidc.TokenSignatureSigningParametersResolver">
- <property name="activationCondition">
- <ref bean="IDTokenRequested" />
- </property>
- <property name="securityParametersContextLookupStrategy">
- <bean parent="shibboleth.Functions.Compose"
- c:g-ref="shibboleth.ChildLookupOrCreate.SecurityParameters"
- c:f-ref="shibboleth.ChildLookup.RelyingParty" />
- </property>
- <property name="existingParametersContextLookupStrategy">
- <bean parent="shibboleth.Functions.Compose"
- c:g-ref="shibboleth.ChildLookup.SecurityParameters"
- c:f-ref="shibboleth.MessageContextLookup.Outbound" />
- </property>
- </bean>
+ <bean id="ValidateAudience"
+ class="net.shibboleth.idp.plugin.oidc.op.oauth2.profile.impl.ValidateAudience" scope="prototype"
+ p:requestedAudienceLookupStrategy-ref="AuthenticationRequestAudienceLookupStrategy"
+ p:allowedAudienceLookupStrategy="#{getObject('shibboleth.oidc.AllowedAudienceStrategy') ?: getObject('shibboleth.oidc.DefaultAllowedAudienceStrategy')}" />
- <bean id="PopulateIDTokenEncryptionParameters"
- class="net.shibboleth.idp.plugin.oidc.op.profile.impl.PopulateOIDCEncryptionParameters" scope="prototype"
- p:configurationLookupStrategy-ref="shibboleth.EncryptionConfigurationLookup"
- p:encryptionParametersResolver-ref="shibboleth.oidc.EncryptionParametersResolver">
- <property name="activationCondition">
- <ref bean="IDTokenRequested" />
- </property>
- </bean>
+ <bean id="AuthenticationRequestAudienceLookupStrategy"
+ class="net.shibboleth.idp.plugin.oidc.op.profile.context.navigate.DefaultRequestAudienceLookupFunction" />
<bean id="shibboleth.oidc.EncryptionParametersResolver"
class="net.shibboleth.idp.plugin.oidc.op.security.impl.OIDCClientInformationEncryptionParametersResolver"
@@ -219,6 +195,171 @@
class="org.opensaml.storage.impl.client.PopulateClientStorageLoadContext" scope="prototype"
p:storageServices="#{ getObject('shibboleth.ClientStorageServices') ?: getObject('shibboleth.DefaultClientStorageServices') }" />
+ <bean id="RevokeConsent" class="net.shibboleth.idp.plugin.oidc.op.profile.impl.RevokeConsent" scope="prototype" />
+
+ <bean id="SetAuthenticationTimeToResponseContext"
+ class="net.shibboleth.idp.plugin.oidc.op.profile.impl.SetAuthenticationTimeToResponseContext" scope="prototype" />
+
+ <bean id="SetSectorIdentifierForAttributeResolution"
+ class="net.shibboleth.idp.plugin.oidc.op.profile.impl.SetSectorIdentifierForAttributeResolution" scope="prototype" />
+
+ <bean id="SetAuthenticationContextClassReferenceToResponseContext"
+ class="net.shibboleth.idp.plugin.oidc.op.profile.impl.SetAuthenticationContextClassReferenceToResponseContext"
+ scope="prototype" />
+
+ <!--
+ Do a metadata lookup for the primary audience of the token for encryption purposes.
+ Contexts are stored under the outbound MessageContext, including the new RelyingPartyContext.
+ -->
+
+ <bean id="AudienceOIDCMetadataLookup" class="net.shibboleth.idp.profile.impl.WebFlowMessageHandlerAdaptor"
+ scope="prototype" c:executionDirection="OUTBOUND">
+ <constructor-arg name="messageHandler">
+ <bean class="net.shibboleth.idp.plugin.oidc.op.profile.impl.OIDCMetadataLookupHandler" scope="prototype">
+ <property name="clientInformationResolver">
+ <ref bean="shibboleth.ClientInformationResolver" />
+ </property>
+ <property name="clientIDLookupStrategy">
+ <ref bean="AudienceClientIDLookupStrategy" />
+ </property>
+ </bean>
+ </constructor-arg>
+ </bean>
+
+ <bean id="AudienceClientIDLookupStrategy"
+ class="net.shibboleth.idp.plugin.oidc.op.messaging.context.navigate.AudienceClientIDLookupFunction" />
+
+ <bean id="InitializeAudienceRelyingPartyContext"
+ class="net.shibboleth.idp.plugin.oidc.op.profile.impl.InitializeRelyingPartyContext" scope="prototype"
+ p:relyingPartyContextCreationStrategy-ref="AudienceRelyingPartyCreationStrategy"
+ p:oidcMetadataContextLookupStrategy-ref="LookupOutboundOIDCMetadataContext"
+ p:clientIDLookupStrategy-ref="AudienceClientIDLookupStrategy"
+ p:inbound="false" />
+
+ <bean id="LookupOutboundOIDCMetadataContext"
+ class="net.shibboleth.idp.plugin.oidc.op.profile.context.navigate.DefaultOIDCMetadataContextLookupFunction"
+ p:inbound="false" />
+
+ <bean id="AudienceRelyingPartyCreationStrategy" parent="shibboleth.Functions.Compose"
+ c:g-ref="shibboleth.ChildLookupOrCreate.RelyingPartyContext"
+ c:f-ref="shibboleth.MessageContextLookup.Outbound" />
+
+ <bean id="AudienceSAMLProtocolAndRole"
+ class="net.shibboleth.idp.profile.impl.WebFlowMessageHandlerAdaptor" scope="prototype"
+ c:executionDirection="OUTBOUND"
+ p:activationCondition-ref="%{idp.oidc.metadata.saml:shibboleth.Conditions.TRUE}">
+ <constructor-arg name="messageHandler">
+ <bean class="org.opensaml.saml.common.binding.impl.SAMLProtocolAndRoleHandler" scope="prototype"
+ p:protocol="http://openid.net/specs/openid-connect-core-1_0.html"
+ p:role-ref="shibboleth.MetadataLookup.Role"
+ p:entityContextClass="org.opensaml.saml.common.messaging.context.SAMLPeerEntityContext"/>
+ </constructor-arg>
+ </bean>
+
+ <bean id="SetAudienceEntityIdToSAMLPeerEntityContext"
+ class="net.shibboleth.idp.profile.impl.WebFlowMessageHandlerAdaptor" scope="prototype"
+ c:executionDirection="OUTBOUND"
+ p:errorEvent="#{T(org.opensaml.profile.action.EventIds).INVALID_MSG_CTX}"
+ p:activationCondition-ref="%{idp.oidc.metadata.saml:shibboleth.Conditions.TRUE}">
+ <constructor-arg name="messageHandler">
+ <bean class="net.shibboleth.idp.plugin.oidc.op.oauth2.messaging.impl.SetEntityIdToSAMLPeerEntityContext"
+ scope="prototype"
+ p:clientIDLookupStrategy-ref="AudienceClientIDLookupStrategy"
+ p:entityContextClass="org.opensaml.saml.common.messaging.context.SAMLPeerEntityContext" />
+ </constructor-arg>
+ </bean>
+
+ <bean id="AudienceSAMLMetadataLookup"
+ class="net.shibboleth.idp.profile.impl.WebFlowMessageHandlerAdaptor" scope="prototype"
+ c:executionDirection="OUTBOUND"
+ p:activationCondition-ref="%{idp.oidc.metadata.saml:shibboleth.Conditions.TRUE}">
+ <constructor-arg name="messageHandler">
+ <bean class="org.opensaml.saml.common.binding.impl.SAMLMetadataLookupHandler" scope="prototype"
+ p:entityContextClass="org.opensaml.saml.common.messaging.context.SAMLPeerEntityContext">
+ <property name="roleDescriptorResolver">
+ <bean class="org.opensaml.saml.metadata.resolver.impl.PredicateRoleDescriptorResolver"
+ c:mdResolver-ref="shibboleth.MetadataResolver" />
+ </property>
+ </bean>
+ </constructor-arg>
+ </bean>
+
+ <bean id="PopulateAudienceOIDCMetadataContext"
+ class="net.shibboleth.idp.profile.impl.WebFlowMessageHandlerAdaptor" scope="prototype"
+ c:executionDirection="OUTBOUND"
+ p:activationCondition-ref="%{idp.oidc.metadata.saml:shibboleth.Conditions.TRUE}">
+ <constructor-arg name="messageHandler">
+ <bean class="net.shibboleth.idp.plugin.oidc.op.oauth2.messaging.impl.PopulateOIDCMetadataContext"
+ scope="prototype" />
+ </constructor-arg>
+ </bean>
+
+ <bean id="InitializeAudienceRelyingPartyContextFromSAMLPeer"
+ class="net.shibboleth.idp.saml.profile.impl.InitializeRelyingPartyContextFromSAMLPeer" scope="prototype"
+ p:relyingPartyContextCreationStrategy-ref="AudienceRelyingPartyCreationStrategy"
+ p:peerEntityContextLookupStrategy-ref="LookupOutboundPeerEntityContext" />
+
+ <bean id="LookupOutboundPeerEntityContext" parent="shibboleth.Functions.Compose"
+ c:g-ref="shibboleth.ChildLookup.SAMLPeerEntityContext"
+ c:f-ref="shibboleth.MessageContextLookup.Outbound"/>
+
+ <bean id="SelectAudienceRelyingPartyConfiguration"
+ class="net.shibboleth.idp.profile.impl.SelectRelyingPartyConfiguration" scope="prototype"
+ p:relyingPartyContextLookupStrategy-ref="AudienceRelyingPartyCreationStrategy"
+ p:relyingPartyConfigurationResolver-ref="shibboleth.CriteriaRelyingPartyConfigurationResolver" />
+
+ <bean id="SelectAudienceProfileConfiguration"
+ class="net.shibboleth.idp.profile.impl.SelectProfileConfiguration" scope="prototype"
+ p:relyingPartyContextLookupStrategy-ref="AudienceRelyingPartyCreationStrategy"
+ p:profileId="#{T(net.shibboleth.oidc.profile.oauth2.config.OAuth2TokenAudienceConfiguration).PROFILE_ID}" />
+
+ <bean id="ResolveAttributesForAudience" class="net.shibboleth.idp.profile.impl.ResolveAttributes" scope="prototype"
+ c:resolverService-ref="shibboleth.AttributeResolverService"
+ p:transcoderRegistry-ref="shibboleth.AttributeRegistryService"
+ p:maskFailures="%{idp.service.attribute.resolver.maskFailures:true}"
+ p:issuerLookupStrategy-ref="AudienceIssuerLookupFunction" />
+
+ <bean id="FilterAttributesForAudience" class="net.shibboleth.idp.profile.impl.FilterAttributes" scope="prototype"
+ c:filterService-ref="shibboleth.AttributeFilterService"
+ p:maskFailures="%{idp.service.attribute.filter.maskFailures:true}"
+ p:issuerLookupStrategy-ref="AudienceIssuerLookupFunction"
+ p:proxiedRequesterContextLookupStrategy-ref="AudienceProxiedRequesterLookupFunction"
+ p:proxiedRequesterMetadataContextLookupStrategy-ref="LookupOutboundSAMLEntityContext" />
+
+ <bean id="AudienceIssuerLookupFunction"
+ class="net.shibboleth.idp.profile.context.navigate.ResponderIdLookupFunction"
+ p:relyingPartyContextLookupStrategy-ref="AudienceRelyingPartyCreationStrategy" />
+
+ <bean id="AudienceProxiedRequesterLookupFunction" parent="shibboleth.Functions.Compose">
+ <constructor-arg name="g">
+ <bean class="org.opensaml.messaging.context.navigate.ChildContextLookup"
+ c:type="#{ T(org.opensaml.profile.context.ProxiedRequesterContext) }" />
+ </constructor-arg>
+ <constructor-arg name="f">
+ <ref bean="shibboleth.MessageContextLookup.Outbound" />
+ </constructor-arg>
+ </bean>
+
+ <bean id="LookupOutboundSAMLEntityContext" parent="shibboleth.Functions.Compose"
+ c:g-ref="shibboleth.ChildLookup.SAMLMetadataContext"
+ c:f-ref="LookupOutboundPeerEntityContext"/>
+
+ <!-- Back to token prep. -->
+
+ <bean id="SetSubjectToResponseContext"
+ class="net.shibboleth.idp.plugin.oidc.op.profile.impl.SetSubjectToResponseContext" scope="prototype">
+ <property name="subjectLookupStrategy">
+ <bean class="net.shibboleth.idp.plugin.oidc.op.profile.logic.AttributeResolutionSubjectLookupFunction"
+ p:transcoderRegistry-ref="shibboleth.AttributeRegistryService" />
+ </property>
+ <property name="subjectTypeLookupStrategy">
+ <bean class="net.shibboleth.idp.plugin.oidc.op.profile.logic.DefaultSubjectTypeStrategy" />
+ </property>
+ </bean>
+
+ <bean id="VerifyRequestedSubjectIdentifier"
+ class="net.shibboleth.idp.plugin.oidc.op.profile.impl.VerifyRequestedSubjectIdentifier" scope="prototype" />
+
<bean id="SetTokenDeliveryAttributesToResponseContext"
class="net.shibboleth.idp.plugin.oidc.op.profile.impl.SetTokenDeliveryAttributesToResponseContext" scope="prototype"
p:transcoderRegistry-ref="shibboleth.AttributeRegistryService">
@@ -237,8 +378,6 @@
<bean id="SetConsentToResponseContext"
class="net.shibboleth.idp.plugin.oidc.op.profile.impl.SetConsentToResponseContext" scope="prototype" />
- <bean id="RevokeConsent" class="net.shibboleth.idp.plugin.oidc.op.profile.impl.RevokeConsent" scope="prototype" />
-
<bean id="SetAuthorizationCodeToResponseContext"
class="net.shibboleth.idp.plugin.oidc.op.profile.impl.SetAuthorizationCodeToResponseContext" scope="prototype"
p:dataSealer-ref="#{'%{idp.oidc.tokenSealer:shibboleth.oidc.TokenSealer}'.trim()}">
@@ -251,106 +390,147 @@
</property>
</bean>
- <bean id="SetAccessTokenToResponseContext"
- class="net.shibboleth.idp.plugin.oidc.op.profile.impl.SetAccessTokenToResponseContext" scope="prototype"
- p:dataSealer-ref="#{'%{idp.oidc.tokenSealer:shibboleth.oidc.TokenSealer}'.trim()}">
- <property name="activationCondition">
- <ref bean="AccessTokenRequested" />
+ <!-- If access token is strictly for UserInfo endpoint... -->
+
+ <bean id="PopulateUserInfoAccessTokenSignatureSigningParameters"
+ class="net.shibboleth.idp.plugin.oidc.op.profile.impl.PopulateOIDCSignatureSigningParameters"
+ scope="prototype"
+ c:strategy-ref="shibboleth.MessageContextLookup.Inbound"
+ p:configurationLookupStrategy-ref="shibboleth.SignatureSigningConfigurationLookup"
+ p:signatureSigningParametersResolver-ref="shibboleth.oidc.TokenSignatureSigningParametersResolver">
+ <property name="securityParametersContextLookupStrategy">
+ <bean parent="shibboleth.Functions.Compose"
+ c:g-ref="shibboleth.ChildLookupOrCreate.SecurityParameters"
+ c:f-ref="shibboleth.ChildLookup.RelyingParty" />
</property>
</bean>
- <bean id="SetSubjectToResponseContext"
- class="net.shibboleth.idp.plugin.oidc.op.profile.impl.SetSubjectToResponseContext" scope="prototype">
- <property name="subjectLookupStrategy">
- <bean class="net.shibboleth.idp.plugin.oidc.op.profile.logic.AttributeResolutionSubjectLookupFunction"
- p:transcoderRegistry-ref="shibboleth.AttributeRegistryService" />
- </property>
- <property name="subjectTypeLookupStrategy">
- <bean class="net.shibboleth.idp.plugin.oidc.op.profile.logic.DefaultSubjectTypeStrategy" />
+ <bean id="BuildOIDCAccessToken"
+ class="net.shibboleth.idp.plugin.oidc.op.oauth2.profile.impl.BuildAccessToken" scope="prototype"
+ p:dataSealer="#{getObject('%{idp.oidc.tokenSealer:shibboleth.oidc.TokenSealer}'.trim())}"
+ p:clientIDLookupStrategy-ref="RequestClientIDLookup" />
+
+ <bean id="RequestClientIDLookup" parent="shibboleth.Functions.Compose"
+ c:g-ref="shibboleth.ClientIDLookupStrategy"
+ c:f-ref="shibboleth.MessageContextLookup.Inbound" />
+
+ <bean id="SignOIDCAccessToken"
+ class="net.shibboleth.idp.plugin.oidc.op.oauth2.profile.impl.SignAccessToken" scope="prototype"
+ p:typeHeader="at+jwt">
+ <property name="securityParametersLookupStrategy">
+ <bean parent="shibboleth.Functions.Compose"
+ c:g-ref="shibboleth.ChildLookup.SecurityParameters"
+ c:f-ref="shibboleth.ChildLookup.RelyingParty" />
</property>
</bean>
+
+ <bean id="SetOAuthAccessTokenToResponseContext"
+ class="net.shibboleth.idp.plugin.oidc.op.oauth2.profile.impl.SetAccessTokenToResponseContext"
+ scope="prototype" />
- <bean id="SetAuthenticationTimeToResponseContext"
- class="net.shibboleth.idp.plugin.oidc.op.profile.impl.SetAuthenticationTimeToResponseContext" scope="prototype" />
-
- <bean id="SetSectorIdentifierForAttributeResolution"
- class="net.shibboleth.idp.plugin.oidc.op.profile.impl.SetSectorIdentifierForAttributeResolution" scope="prototype" />
+ <!-- If access token is also for third-party resource... -->
- <bean id="SetAuthenticationContextClassReferenceToResponseContext"
- class="net.shibboleth.idp.plugin.oidc.op.profile.impl.SetAuthenticationContextClassReferenceToResponseContext"
- scope="prototype" />
+ <bean id="PopulateThirdPartyAccessTokenSignatureSigningParameters"
+ class="net.shibboleth.idp.plugin.oidc.op.profile.impl.PopulateOIDCSignatureSigningParameters"
+ scope="prototype"
+ c:strategy-ref="shibboleth.MessageContextLookup.Outbound"
+ p:configurationLookupStrategy-ref="shibboleth.SignatureSigningConfigurationLookup"
+ p:signatureSigningParametersResolver-ref="shibboleth.oidc.TokenSignatureSigningParametersResolver"
+ p:securityParametersContextLookupStrategy-ref="AudienceSecurityParametersCreationStrategy" />
+
+ <bean id="AudienceSecurityParametersCreationStrategy" parent="shibboleth.Functions.Compose"
+ c:g-ref="shibboleth.ChildLookupOrCreate.SecurityParameters"
+ c:f-ref="AudienceRelyingPartyCreationStrategy" />
+
+ <bean id="AddAttributeClaimsToAccessToken"
+ class="net.shibboleth.idp.plugin.oidc.op.profile.impl.AddAttributesToClaimsSet" scope="prototype"
+ p:transcoderRegistry-ref="shibboleth.AttributeRegistryService"
+ p:responseClaimsSetLookupStrategy-ref="AccessTokenClaimsSetLookupFunction"
+ p:reservedClaimNames="#{getObject('shibboleth.oidc.AccessTokenReservedClaimNames') ?: getObject('shibboleth.oidc.DefaultAccessTokenReservedClaimNames')}" />
+
+ <bean id="AccessTokenClaimsSetLookupFunction"
+ class="net.shibboleth.idp.plugin.oidc.op.profile.context.navigate.AccessTokenClaimsSetLookupFunction"
+ p:autoCreate="true" />
+
+ <bean id="BuildAccessToken"
+ class="net.shibboleth.idp.plugin.oidc.op.oauth2.profile.impl.BuildAccessToken" scope="prototype"
+ p:dataSealer="#{getObject('%{idp.oidc.tokenSealer:shibboleth.oidc.TokenSealer}'.trim())}"
+ p:issuerLookupStrategy-ref="AudienceIssuerLookupFunction"
+ p:clientIDLookupStrategy-ref="RequestClientIDLookup"
+ p:accessTokenTypeLookupStrategy-ref="AccessTokenTypeLookupFunction"
+ p:accessTokenLifetimeLookupStrategy-ref="AccessTokenLifetimeLookupFunction" />
+
+ <bean id="AccessTokenTypeLookupFunction"
+ class="net.shibboleth.oidc.profile.config.navigate.AccessTokenTypeLookupFunction"
+ p:relyingPartyContextLookupStrategy-ref="AudienceRelyingPartyCreationStrategy" />
+
+ <bean id="AccessTokenLifetimeLookupFunction"
+ class="net.shibboleth.oidc.profile.config.navigate.AccessTokenLifetimeLookupFunction"
+ p:relyingPartyContextLookupStrategy-ref="AudienceRelyingPartyCreationStrategy" />
+
+ <bean id="SignAccessToken"
+ class="net.shibboleth.idp.plugin.oidc.op.oauth2.profile.impl.SignAccessToken" scope="prototype"
+ p:securityParametersLookupStrategy-ref="AudienceSecurityParametersCreationStrategy"
+ p:typeHeader="at+jwt" />
+
+ <!-- ID token actions. -->
- <bean id="AddIDTokenShell" class="net.shibboleth.idp.plugin.oidc.op.profile.impl.AddIDTokenShell" scope="prototype">
- <property name="activationCondition">
- <ref bean="IDTokenRequested" />
+ <bean id="PopulateIDTokenSignatureSigningParameters"
+ class="net.shibboleth.idp.plugin.oidc.op.profile.impl.PopulateOIDCSignatureSigningParameters " scope="prototype"
+ c:strategy-ref="shibboleth.MessageContextLookup.Inbound"
+ p:configurationLookupStrategy-ref="shibboleth.SignatureSigningConfigurationLookup"
+ p:signatureSigningParametersResolver-ref="shibboleth.oidc.TokenSignatureSigningParametersResolver">
+ <property name="securityParametersContextLookupStrategy">
+ <bean parent="shibboleth.Functions.Compose"
+ c:g-ref="shibboleth.ChildLookupOrCreate.SecurityParameters"
+ c:f-ref="shibboleth.ChildLookup.RelyingParty" />
+ </property>
+ <property name="existingParametersContextLookupStrategy">
+ <bean parent="shibboleth.Functions.Compose"
+ c:g-ref="shibboleth.ChildLookup.SecurityParameters"
+ c:f-ref="shibboleth.MessageContextLookup.Outbound" />
</property>
</bean>
+ <bean id="PopulateIDTokenEncryptionParameters"
+ class="net.shibboleth.idp.plugin.oidc.op.profile.impl.PopulateOIDCEncryptionParameters" scope="prototype"
+ p:configurationLookupStrategy-ref="shibboleth.EncryptionConfigurationLookup"
+ p:encryptionParametersResolver-ref="shibboleth.oidc.EncryptionParametersResolver" />
+
+ <bean id="AddIDTokenShell" class="net.shibboleth.idp.plugin.oidc.op.profile.impl.AddIDTokenShell" scope="prototype" />
+
<bean id="AddAttributeClaimsToIDToken"
class="net.shibboleth.idp.plugin.oidc.op.profile.impl.AddAttributesToClaimsSet" scope="prototype"
p:transcoderRegistry-ref="shibboleth.AttributeRegistryService"
- p:reservedClaimNames="#{getObject('shibboleth.oidc.IDTokenReservedClaimNames') ?: getObject('shibboleth.oidc.DefaultIDTokenReservedClaimNames')}">
- <property name="activationCondition">
- <ref bean="IDTokenRequested" />
- </property>
- </bean>
+ p:reservedClaimNames="#{getObject('shibboleth.oidc.IDTokenReservedClaimNames') ?: getObject('shibboleth.oidc.DefaultIDTokenReservedClaimNames')}" />
<bean id="AddAuthTimeToIDToken" class="net.shibboleth.idp.plugin.oidc.op.profile.impl.AddAuthTimeToIDToken"
- scope="prototype">
- <property name="activationCondition">
- <ref bean="IDTokenRequested" />
- </property>
- </bean>
+ scope="prototype" />
- <bean id="AddAcrToIDToken" class="net.shibboleth.idp.plugin.oidc.op.profile.impl.AddAcrToIDToken" scope="prototype">
- <property name="activationCondition">
- <ref bean="IDTokenRequested" />
- </property>
- </bean>
+ <bean id="AddAcrToIDToken" class="net.shibboleth.idp.plugin.oidc.op.profile.impl.AddAcrToIDToken"
+ scope="prototype" />
<bean id="AddNonceToIDToken" class="net.shibboleth.idp.plugin.oidc.op.profile.impl.AddNonceToIDToken"
- scope="prototype">
- <property name="activationCondition">
- <ref bean="IDTokenRequested" />
- </property>
- </bean>
+ scope="prototype" />
<bean id="AddAccessTokenHashToIDToken"
- class="net.shibboleth.idp.plugin.oidc.op.profile.impl.AddAccessTokenHashToIDToken" scope="prototype">
+ class="net.shibboleth.idp.plugin.oidc.op.profile.impl.AddAccessTokenHashToIDToken" scope="prototype"
+ p:activationCondition-ref="AccessTokenRequested">
<property name="securityParametersLookupStrategy">
<bean parent="shibboleth.Functions.Compose"
c:g-ref="shibboleth.ChildLookup.SecurityParameters"
c:f-ref="shibboleth.ChildLookup.RelyingParty" />
</property>
- <property name="activationCondition">
- <bean parent="shibboleth.Conditions.AND">
- <constructor-arg>
- <ref bean="AccessTokenRequested" />
- </constructor-arg>
- <constructor-arg>
- <ref bean="IDTokenRequested" />
- </constructor-arg>
- </bean>
- </property>
</bean>
<bean id="AddAuthorizationCodeHashToIDToken"
- class="net.shibboleth.idp.plugin.oidc.op.profile.impl.AddAuthorizationCodeHashToIDToken" scope="prototype">
+ class="net.shibboleth.idp.plugin.oidc.op.profile.impl.AddAuthorizationCodeHashToIDToken" scope="prototype"
+ p:activationCondition-ref="AuthorizeCodeRequested">
<property name="securityParametersLookupStrategy">
<bean parent="shibboleth.Functions.Compose"
c:g-ref="shibboleth.ChildLookup.SecurityParameters"
c:f-ref="shibboleth.ChildLookup.RelyingParty" />
</property>
- <property name="activationCondition">
- <bean parent="shibboleth.Conditions.AND">
- <constructor-arg>
- <ref bean="AuthorizeCodeRequested" />
- </constructor-arg>
- <constructor-arg>
- <ref bean="IDTokenRequested" />
- </constructor-arg>
- </bean>
- </property>
</bean>
<bean id="SignIDToken" class="net.shibboleth.idp.plugin.oidc.op.profile.impl.SignIDToken" scope="prototype">
@@ -359,17 +539,10 @@
c:g-ref="shibboleth.ChildLookup.SecurityParameters"
c:f-ref="shibboleth.ChildLookup.RelyingParty" />
</property>
- <property name="activationCondition">
- <ref bean="IDTokenRequested" />
- </property>
</bean>
<bean id="EncryptIDToken" class="net.shibboleth.idp.plugin.oidc.op.profile.impl.EncryptProcessedToken"
- scope="prototype">
- <property name="activationCondition">
- <ref bean="IDTokenRequested" />
- </property>
- </bean>
+ scope="prototype" />
<bean id="UpdateSessionWithSPSession"
class="net.shibboleth.idp.session.impl.UpdateSessionWithSPSession" scope="prototype"
diff --git a/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/authorize/authorize-flow.xml b/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/authorize/authorize-flow.xml
index 45b92e20..48096df1 100644
--- a/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/authorize/authorize-flow.xml
+++ b/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/authorize/authorize-flow.xml
@@ -50,10 +50,9 @@
<evaluate expression="ValidateResponseType" />
<evaluate expression="ValidateCodeChallenge" />
<evaluate expression="ValidateScope" />
+ <evaluate expression="ValidateAudience" />
<evaluate expression="SetRequestedClaimsToResponseContext" />
<evaluate expression="SetRequestedSubjectToResponseContext" />
- <evaluate expression="PopulateIDTokenSignatureSigningParameters" />
- <evaluate expression="PopulateIDTokenEncryptionParameters" />
<evaluate expression="'proceed'" />
<transition on="proceed" to="PopulateClientStorageLoadContext" />
</action-state>
@@ -86,17 +85,81 @@
<action-state id="SetAuthenticationInformationToResponseContext">
<evaluate expression="SetAuthenticationContextClassReferenceToResponseContext" />
<evaluate expression="SetAuthenticationTimeToResponseContext" />
+ <evaluate expression="SetSectorIdentifierForAttributeResolution" />
<evaluate expression="'proceed'" />
- <transition on="proceed" to="AttributeResolution" />
+ <transition on="proceed" to="CheckForAudience" />
</action-state>
- <action-state id="AttributeResolution">
- <evaluate expression="SetSectorIdentifierForAttributeResolution" />
+ <!-- Audience may or may not be a factor. -->
+ <decision-state id="CheckForAudience">
+ <if test="opensamlProfileRequestContext.getOutboundMessageContext().getSubcontext(T(net.shibboleth.idp.plugin.oidc.op.messaging.context.OIDCAuthenticationResponseContext)).getAudience().isEmpty()"
+ then="AttributeResolutionForClient"
+ else="LookupAudienceMetadata" />
+ </decision-state>
+
+ <!-- May need to add a second Relying Party for the primary resource/audience. -->
+
+ <action-state id="LookupAudienceMetadata">
+ <evaluate expression="AudienceOIDCMetadataLookup" />
+ <evaluate expression="InitializeAudienceRelyingPartyContext" />
+ <evaluate expression="'proceed'" />
+
+ <transition on="proceed" to="CheckIfAudienceFoundFromClientInformationService" />
+ </action-state>
+
+ <decision-state id="CheckIfAudienceFoundFromClientInformationService">
+ <if test="opensamlProfileRequestContext.getOutboundMessageContext().containsSubcontext(T(net.shibboleth.oidc.metadata.context.OIDCMetadataContext))"
+ then="SelectAudienceProfileConfiguration" else="LookupAudienceSAMLMetadata" />
+ </decision-state>
+
+ <action-state id="LookupAudienceSAMLMetadata">
+ <evaluate expression="AudienceSAMLProtocolAndRole" />
+ <evaluate expression="SetAudienceEntityIdToSAMLPeerEntityContext" />
+ <evaluate expression="AudienceSAMLMetadataLookup" />
+ <evaluate expression="'proceed'" />
+
+ <transition on="proceed" to="CheckIfAudienceFoundFromSAMLMetadata" />
+ </action-state>
+
+ <decision-state id="CheckIfAudienceFoundFromSAMLMetadata">
+ <if test="opensamlProfileRequestContext.getOutboundMessageContext().containsSubcontext(T(org.opensaml.saml.common.messaging.context.SAMLPeerEntityContext)) and opensamlProfileRequestContext.getOutboundMessageContext().getSubcontext(T(org.opensaml.saml.common.messaging.context.SAMLPeerEntityContext)).containsSubcontext(T(org.opensaml.saml.common.messaging.context.SAMLMetadataContext))"
+ then="PopulateAudienceOIDCMetadataContextFromSAML"
+ else="SelectAudienceProfileConfiguration" />
+ </decision-state>
+
+ <action-state id="PopulateAudienceOIDCMetadataContextFromSAML">
+ <evaluate expression="PopulateAudienceOIDCMetadataContext" />
+ <evaluate expression="InitializeAudienceRelyingPartyContextFromSAMLPeer" />
+ <evaluate expression="'proceed'" />
+
+ <transition on="proceed" to="SelectAudienceProfileConfiguration" />
+ </action-state>
+
+ <action-state id="SelectAudienceProfileConfiguration">
+ <evaluate expression="SelectAudienceRelyingPartyConfiguration" />
+ <evaluate expression="SelectAudienceProfileConfiguration" />
+ <evaluate expression="'proceed'" />
+
+ <transition on="proceed" to="AttributeResolutionForAudience" />
+ </action-state>
+
+ <action-state id="AttributeResolutionForClient">
<evaluate expression="ResolveAttributes" />
<evaluate expression="FilterAttributes" />
<evaluate expression="RevokeConsent" />
<evaluate expression="PopulatePostAuthnInterceptContext" />
<evaluate expression="'proceed'" />
+
+ <transition on="proceed" to="CheckPostAuthnInterceptContext" />
+ </action-state>
+
+ <action-state id="AttributeResolutionForAudience">
+ <evaluate expression="ResolveAttributesForAudience" />
+ <evaluate expression="FilterAttributesForAudience" />
+ <evaluate expression="RevokeConsent" />
+ <evaluate expression="PopulatePostAuthnInterceptContext" />
+ <evaluate expression="'proceed'" />
+
<transition on="proceed" to="CheckPostAuthnInterceptContext" />
</action-state>
@@ -116,19 +179,75 @@
<evaluate expression="SetSubjectToResponseContext" />
<evaluate expression="VerifyRequestedSubjectIdentifier" />
<evaluate expression="'proceed'" />
- <transition on="proceed" to="BuildTokens" />
+
+ <transition on="proceed" to="BuildCode" />
</action-state>
- <action-state id="BuildTokens">
+ <action-state id="BuildCode">
<evaluate expression="SetTokenDeliveryAttributesToResponseContext" />
<evaluate expression="SetConsentToResponseContext" />
<evaluate expression="SetAuthorizationCodeToResponseContext" />
- <evaluate expression="SetAccessTokenToResponseContext" />
<evaluate expression="'proceed'" />
- <transition on="proceed" to="BuildResponse" />
+
+ <transition on="proceed" to="CheckIfAccessTokenNeeded" />
+ </action-state>
+
+ <decision-state id="CheckIfAccessTokenNeeded">
+ <if test="AccessTokenRequested.test(opensamlProfileRequestContext)"
+ then="CheckTokenRequirements"
+ else="CheckIfIDTokenNeeded" />
+ </decision-state>
+
+ <decision-state id="CheckTokenRequirements">
+ <if test="opensamlProfileRequestContext.getOutboundMessageContext().getSubcontext(T(net.shibboleth.idp.plugin.oidc.op.messaging.context.OIDCAuthenticationResponseContext)).getAudience().isEmpty()"
+ then="BuildTokensForUserInfoAccess"
+ else="BuildTokensForThirdPartyAccess" />
+ </decision-state>
+
+ <!--
+ Note no JWT encryption here. The token shouldn't even be a JWT, but even if it is,
+ the only audience is us, and the client can't be expected to decrypt it, so it
+ wouldn't be usable. If it were encrypted to our key then there would be no point to
+ allowing it to be a JWT.
+
+ Note also no attribute claims are added to the access token since that isn't a proper
+ delivery mechanism for claims to the OIDC client.
+ -->
+ <action-state id="BuildTokensForUserInfoAccess">
+ <evaluate expression="PopulateUserInfoAccessTokenSignatureSigningParameters" />
+ <evaluate expression="BuildOIDCAccessToken" />
+ <evaluate expression="SignOIDCAccessToken" />
+ <evaluate expression="SetOAuthAccessTokenToResponseContext" />
+ <evaluate expression="'proceed'" />
+
+ <transition on="proceed" to="CheckIfIDTokenNeeded" />
+ </action-state>
+
+ <!--
+ This includes OIDC use cases and all grant types but the primary audience
+ for the access token is the resource/audience. Encryption remains impossible until
+ this supports pure OAuth scenarios.
+ -->
+ <action-state id="BuildTokensForThirdPartyAccess">
+ <evaluate expression="PopulateThirdPartyAccessTokenSignatureSigningParameters" />
+ <evaluate expression="AddAttributeClaimsToAccessToken" />
+ <evaluate expression="BuildAccessToken" />
+ <evaluate expression="SignAccessToken" />
+ <evaluate expression="SetOAuthAccessTokenToResponseContext" />
+ <evaluate expression="'proceed'" />
+
+ <transition on="proceed" to="CheckIfIDTokenNeeded" />
</action-state>
- <action-state id="BuildResponse">
+ <decision-state id="CheckIfIDTokenNeeded">
+ <if test="IDTokenRequested.test(opensamlProfileRequestContext)"
+ then="BuildIDToken"
+ else="PopulateClientStorageSaveContext" />
+ </decision-state>
+
+ <action-state id="BuildIDToken">
+ <evaluate expression="PopulateIDTokenSignatureSigningParameters" />
+ <evaluate expression="PopulateIDTokenEncryptionParameters" />
<evaluate expression="AddIDTokenShell" />
<evaluate expression="AddAttributeClaimsToIDToken" />
<evaluate expression="AddAuthTimeToIDToken" />
@@ -138,12 +257,12 @@
<evaluate expression="AddAuthorizationCodeHashToIDToken" />
<evaluate expression="SignIDToken" />
<evaluate expression="EncryptIDToken" />
- <evaluate expression="UpdateSessionWithSPSession" />
<evaluate expression="'proceed'" />
<transition on="proceed" to="PopulateClientStorageSaveContext" />
</action-state>
<action-state id="PopulateClientStorageSaveContext">
+ <evaluate expression="UpdateSessionWithSPSession" />
<evaluate expression="PopulateClientStorageSaveContext" />
<evaluate expression="'proceed'" />
<transition on="proceed" to="ClientStorageSave" />
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 2ce9c598..4d4f0962 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
@@ -162,16 +162,12 @@
p:requestedScopeLookupStrategy-ref="TokenRequestScopeLookupStrategy"
p:allowedScopeLookupStrategy="#{getObject('shibboleth.oidc.AllowedScopeStrategy') ?: getObject('shibboleth.oidc.DefaultAllowedScopeStrategy')}" />
- <bean id="TokenRequestScopeLookupStrategy"
- class="net.shibboleth.idp.plugin.oidc.op.profile.context.navigate.TokenRequestScopeLookupFunction" />
-
<bean id="ValidateAudience"
class="net.shibboleth.idp.plugin.oidc.op.oauth2.profile.impl.ValidateAudience" scope="prototype"
- p:requestedAudienceLookupStrategy-ref="TokenRequestAudienceLookupStrategy"
p:allowedAudienceLookupStrategy="#{getObject('shibboleth.oidc.AllowedAudienceStrategy') ?: getObject('shibboleth.oidc.DefaultAllowedAudienceStrategy')}" />
- <bean id="TokenRequestAudienceLookupStrategy"
- class="net.shibboleth.idp.plugin.oidc.op.profile.context.navigate.TokenRequestAudienceLookupFunction" />
+ <bean id="TokenRequestScopeLookupStrategy"
+ class="net.shibboleth.idp.plugin.oidc.op.profile.context.navigate.TokenRequestScopeLookupFunction" />
<bean id="IssueIDTokenCondition"
class="net.shibboleth.idp.plugin.oidc.op.profile.logic.IssueIDTokenCondition" />
@@ -315,80 +311,106 @@
<!-- Back to token prep. -->
- <!-- Traditional third-party grant response handling. -->
+ <!-- OIDC response handling for access/refresh tokens. -->
- <bean id="PopulateIDTokenSignatureSigningParameters"
+ <bean id="PopulateUserInfoAccessTokenSignatureSigningParameters"
class="net.shibboleth.idp.plugin.oidc.op.profile.impl.PopulateOIDCSignatureSigningParameters"
scope="prototype"
c:strategy-ref="shibboleth.MessageContextLookup.Inbound"
p:configurationLookupStrategy-ref="shibboleth.SignatureSigningConfigurationLookup"
- p:signatureSigningParametersResolver-ref="shibboleth.oidc.TokenSignatureSigningParametersResolver"
- p:activationCondition-ref="IssueIDTokenCondition">
+ p:signatureSigningParametersResolver-ref="shibboleth.oidc.TokenSignatureSigningParametersResolver">
<property name="securityParametersContextLookupStrategy">
<bean parent="shibboleth.Functions.Compose"
c:g-ref="shibboleth.ChildLookupOrCreate.SecurityParameters"
c:f-ref="shibboleth.ChildLookup.RelyingParty" />
</property>
- <property name="existingParametersContextLookupStrategy">
+ </bean>
+
+ <bean id="BuildOIDCAccessToken"
+ class="net.shibboleth.idp.plugin.oidc.op.oauth2.profile.impl.BuildAccessToken" scope="prototype"
+ p:dataSealer="#{getObject('%{idp.oidc.tokenSealer:shibboleth.oidc.TokenSealer}'.trim())}" />
+
+ <bean id="SignOIDCAccessToken"
+ class="net.shibboleth.idp.plugin.oidc.op.oauth2.profile.impl.SignAccessToken" scope="prototype"
+ p:typeHeader="at+jwt">
+ <property name="securityParametersLookupStrategy">
<bean parent="shibboleth.Functions.Compose"
c:g-ref="shibboleth.ChildLookup.SecurityParameters"
- c:f-ref="shibboleth.MessageContextLookup.Outbound" />
+ c:f-ref="shibboleth.ChildLookup.RelyingParty" />
+ </property>
+ </bean>
+
+ <bean id="SetOAuthAccessTokenToResponseContext"
+ class="net.shibboleth.idp.plugin.oidc.op.oauth2.profile.impl.SetAccessTokenToResponseContext"
+ scope="prototype" />
+
+ <bean id="SetRefreshTokenToResponseContext"
+ class="net.shibboleth.idp.plugin.oidc.op.profile.impl.SetRefreshTokenToResponseContext" scope="prototype"
+ c:sealer-ref="#{'%{idp.oidc.tokenSealer:shibboleth.oidc.TokenSealer}'.trim()}">
+ <property name="activationCondition">
+ <bean parent="shibboleth.Conditions.AND">
+ <constructor-arg>
+ <list>
+ <ref bean="IssueIDTokenCondition" />
+ <bean class="net.shibboleth.oidc.profile.config.logic.RefreshTokensEnabledPredicate" />
+ </list>
+ </constructor-arg>
+ </bean>
+ </property>
+ </bean>
+
+ <!-- ID token actions. -->
+
+ <bean id="PopulateIDTokenSignatureSigningParameters"
+ class="net.shibboleth.idp.plugin.oidc.op.profile.impl.PopulateOIDCSignatureSigningParameters"
+ scope="prototype"
+ c:strategy-ref="shibboleth.MessageContextLookup.Inbound"
+ p:configurationLookupStrategy-ref="shibboleth.SignatureSigningConfigurationLookup"
+ p:signatureSigningParametersResolver-ref="shibboleth.oidc.TokenSignatureSigningParametersResolver">
+ <property name="securityParametersContextLookupStrategy">
+ <bean parent="shibboleth.Functions.Compose"
+ c:g-ref="shibboleth.ChildLookupOrCreate.SecurityParameters"
+ c:f-ref="shibboleth.ChildLookup.RelyingParty" />
</property>
</bean>
<bean id="PopulateIDTokenEncryptionParameters"
class="net.shibboleth.idp.plugin.oidc.op.profile.impl.PopulateOIDCEncryptionParameters" scope="prototype"
p:configurationLookupStrategy-ref="shibboleth.EncryptionConfigurationLookup"
- p:encryptionParametersResolver-ref="shibboleth.oidc.EncryptionParametersResolver"
- p:activationCondition-ref="IssueIDTokenCondition" />
+ p:encryptionParametersResolver-ref="shibboleth.oidc.EncryptionParametersResolver" />
<bean id="shibboleth.oidc.EncryptionParametersResolver"
class="net.shibboleth.idp.plugin.oidc.op.security.impl.OIDCClientInformationEncryptionParametersResolver"
p:remoteJwkSetCache-ref="shibboleth.oidc.RemoteJwkSetCache"
p:keyFetchInterval="%{idp.oidc.jwksuri.fetchInterval:PT30M}" />
- <bean id="SetOIDCAccessTokenToResponseContext"
- class="net.shibboleth.idp.plugin.oidc.op.profile.impl.SetAccessTokenToResponseContext" scope="prototype"
- p:dataSealer-ref="#{'%{idp.oidc.tokenSealer:shibboleth.oidc.TokenSealer}'.trim()}" />
-
- <bean id="SetRefreshTokenToResponseContext"
- class="net.shibboleth.idp.plugin.oidc.op.profile.impl.SetRefreshTokenToResponseContext" scope="prototype"
- c:sealer-ref="#{'%{idp.oidc.tokenSealer:shibboleth.oidc.TokenSealer}'.trim()}">
- <property name="activationCondition">
- <bean class="net.shibboleth.oidc.profile.config.logic.RefreshTokensEnabledPredicate" />
- </property>
- </bean>
-
- <bean id="AddIDTokenShell" class="net.shibboleth.idp.plugin.oidc.op.profile.impl.AddIDTokenShell"
- scope="prototype" p:activationCondition-ref="IssueIDTokenCondition" />
+ <bean id="AddIDTokenShell"
+ class="net.shibboleth.idp.plugin.oidc.op.profile.impl.AddIDTokenShell" scope="prototype" />
<bean id="AddAttributeClaimsToIDToken"
class="net.shibboleth.idp.plugin.oidc.op.profile.impl.AddAttributesToClaimsSet" scope="prototype"
p:transcoderRegistry-ref="shibboleth.AttributeRegistryService"
- p:reservedClaimNames="#{getObject('shibboleth.oidc.IDTokenReservedClaimNames') ?: getObject('shibboleth.oidc.DefaultIDTokenReservedClaimNames')}"
- p:activationCondition-ref="IssueIDTokenCondition" />
+ p:reservedClaimNames="#{getObject('shibboleth.oidc.IDTokenReservedClaimNames') ?: getObject('shibboleth.oidc.DefaultIDTokenReservedClaimNames')}" />
<bean id="AddTokenDeliveryAttributesToIDToken"
class="net.shibboleth.idp.plugin.oidc.op.profile.impl.AddTokenDeliveryAttributesToClaimsSet" scope="prototype"
- p:targetIDToken="true" p:activationCondition-ref="IssueIDTokenCondition" />
+ p:targetIDToken="true" />
- <bean id="AddAuthTimeToIDToken" class="net.shibboleth.idp.plugin.oidc.op.profile.impl.AddAuthTimeToIDToken"
- scope="prototype" p:activationCondition-ref="IssueIDTokenCondition" />
+ <bean id="AddAuthTimeToIDToken"
+ class="net.shibboleth.idp.plugin.oidc.op.profile.impl.AddAuthTimeToIDToken" scope="prototype" />
- <bean id="AddAcrToIDToken" class="net.shibboleth.idp.plugin.oidc.op.profile.impl.AddAcrToIDToken"
- scope="prototype" p:activationCondition-ref="IssueIDTokenCondition" />
+ <bean id="AddAcrToIDToken"
+ class="net.shibboleth.idp.plugin.oidc.op.profile.impl.AddAcrToIDToken" scope="prototype" />
- <bean id="AddNonceToIDToken" class="net.shibboleth.idp.plugin.oidc.op.profile.impl.AddNonceToIDToken"
- scope="prototype" p:requestNonceLookupStrategy-ref="shibboleth.TokenRequestNonceLookupStrategy"
- p:activationCondition-ref="IssueIDTokenCondition" />
+ <bean id="AddNonceToIDToken"
+ class="net.shibboleth.idp.plugin.oidc.op.profile.impl.AddNonceToIDToken" scope="prototype"
+ p:requestNonceLookupStrategy-ref="shibboleth.TokenRequestNonceLookupStrategy" />
<bean id="shibboleth.TokenRequestNonceLookupStrategy"
- class="net.shibboleth.idp.plugin.oidc.op.profile.context.navigate.TokenRequestNonceLookupFunction"
- scope="prototype" />
+ class="net.shibboleth.idp.plugin.oidc.op.profile.context.navigate.TokenRequestNonceLookupFunction" />
<bean id="AddAccessTokenHashToIDToken"
- class="net.shibboleth.idp.plugin.oidc.op.profile.impl.AddAccessTokenHashToIDToken" scope="prototype"
- p:activationCondition-ref="IssueIDTokenCondition">
+ class="net.shibboleth.idp.plugin.oidc.op.profile.impl.AddAccessTokenHashToIDToken" scope="prototype">
<property name="securityParametersLookupStrategy">
<bean parent="shibboleth.Functions.Compose"
c:g-ref="shibboleth.ChildLookup.SecurityParameters"
@@ -396,8 +418,7 @@
</property>
</bean>
- <bean id="SignIDToken" class="net.shibboleth.idp.plugin.oidc.op.profile.impl.SignIDToken" scope="prototype"
- p:activationCondition-ref="IssueIDTokenCondition">
+ <bean id="SignIDToken" class="net.shibboleth.idp.plugin.oidc.op.profile.impl.SignIDToken" scope="prototype">
<property name="securityParametersLookupStrategy">
<bean parent="shibboleth.Functions.Compose"
c:g-ref="shibboleth.ChildLookup.SecurityParameters"
@@ -406,7 +427,7 @@
</bean>
<bean id="EncryptIDToken" class="net.shibboleth.idp.plugin.oidc.op.profile.impl.EncryptProcessedToken"
- p:activationCondition-ref="IssueIDTokenCondition" scope="prototype" />
+ scope="prototype" />
<bean id="FormOutboundMessage"
class="net.shibboleth.idp.plugin.oidc.op.profile.impl.FormOutboundTokenResponseMessage" scope="prototype" />
@@ -420,33 +441,31 @@
</property>
</bean>
- <!-- client_credentials grant response actions. -->
+ <!-- Third-party token actions. -->
- <bean id="PopulateAccessTokenSignatureSigningParameters"
- class="net.shibboleth.idp.plugin.oidc.op.profile.impl.PopulateOIDCSignatureSigningParameters"
- scope="prototype"
- c:strategy-ref="shibboleth.MessageContextLookup.Outbound"
- p:configurationLookupStrategy-ref="shibboleth.SignatureSigningConfigurationLookup"
- p:signatureSigningParametersResolver-ref="shibboleth.oidc.TokenSignatureSigningParametersResolver"
- p:securityParametersContextLookupStrategy-ref="AudienceSecurityParametersCreationStrategy">
- <property name="existingParametersContextLookupStrategy">
- <bean parent="shibboleth.Functions.Compose"
- c:g-ref="shibboleth.ChildLookup.SecurityParameters"
- c:f-ref="shibboleth.MessageContextLookup.Outbound" />
- </property>
- </bean>
+ <bean id="PopulateThirdPartyAccessTokenSignatureSigningParameters"
+ class="net.shibboleth.idp.plugin.oidc.op.profile.impl.PopulateOIDCSignatureSigningParameters"
+ scope="prototype"
+ c:strategy-ref="shibboleth.MessageContextLookup.Outbound"
+ p:configurationLookupStrategy-ref="shibboleth.SignatureSigningConfigurationLookup"
+ p:signatureSigningParametersResolver-ref="shibboleth.oidc.TokenSignatureSigningParametersResolver"
+ p:securityParametersContextLookupStrategy-ref="AudienceSecurityParametersCreationStrategy" />
<bean id="AudienceSecurityParametersCreationStrategy" parent="shibboleth.Functions.Compose"
c:g-ref="shibboleth.ChildLookupOrCreate.SecurityParameters"
c:f-ref="AudienceRelyingPartyCreationStrategy" />
- <bean id="PopulateAccessTokenEncryptionParameters"
- class="net.shibboleth.idp.plugin.oidc.op.profile.impl.PopulateOIDCEncryptionParameters" scope="prototype"
- p:encryptionOptionalPredicate-ref="AudienceEncryptionOptionalPredicate"
- p:oidcMetadataContextLookupStrategy-ref="LookupOutboundOIDCMetadataContext"
- p:configurationLookupStrategy-ref="shibboleth.EncryptionConfigurationLookup"
- p:encryptionParametersResolver-ref="shibboleth.oidc.EncryptionParametersResolver"
- p:encryptionContextLookupStrategy-ref="AudienceEncryptionContextCreationStrategy" />
+ <bean id="PopulateThirdPartyAccessTokenEncryptionParameters"
+ class="net.shibboleth.idp.plugin.oidc.op.profile.impl.PopulateOIDCEncryptionParameters" scope="prototype"
+ p:encryptionOptionalPredicate-ref="AudienceEncryptionOptionalPredicate"
+ p:oidcMetadataContextLookupStrategy-ref="LookupOutboundOIDCMetadataContext"
+ p:configurationLookupStrategy-ref="shibboleth.EncryptionConfigurationLookup"
+ p:encryptionParametersResolver-ref="shibboleth.oidc.EncryptionParametersResolver"
+ p:encryptionContextLookupStrategy-ref="AudienceEncryptionContextCreationStrategy">
+ <property name="activationCondition">
+ <bean parent="shibboleth.Conditions.NOT" c:_0-ref="IssueIDTokenCondition" />
+ </property>
+ </bean>
<bean id="AudienceEncryptionOptionalPredicate"
class="net.shibboleth.oidc.profile.config.logic.EncryptionOptionalPredicate"
@@ -487,13 +506,13 @@
p:typeHeader="at+jwt" />
<bean id="EncryptAccessToken"
- class="net.shibboleth.idp.plugin.oidc.op.oauth2.profile.impl.EncryptAccessToken" scope="prototype"
- p:encryptionContextLookupStrategy-ref="AudienceEncryptionContextCreationStrategy" />
+ class="net.shibboleth.idp.plugin.oidc.op.oauth2.profile.impl.EncryptAccessToken" scope="prototype"
+ p:encryptionContextLookupStrategy-ref="AudienceEncryptionContextCreationStrategy">
+ <property name="activationCondition">
+ <bean parent="shibboleth.Conditions.NOT" c:_0-ref="IssueIDTokenCondition" />
+ </property>
+ </bean>
- <bean id="SetOAuthAccessTokenToResponseContext"
- class="net.shibboleth.idp.plugin.oidc.op.oauth2.profile.impl.SetAccessTokenToResponseContext"
- scope="prototype" />
-
<bean id="PostResponsePopulateAuditContext" parent="shibboleth.AbstractPopulateAuditContext"
p:fieldExtractors="#{getObject('shibboleth.oidc.TokenPostResponseAuditExtractors') ?: getObject('shibboleth.oidc.DefaultTokenPostResponseAuditExtractors')}" />
diff --git a/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/token/token-flow.xml b/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/token/token-flow.xml
index 0f6afca5..a2be67eb 100644
--- a/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/token/token-flow.xml
+++ b/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/token/token-flow.xml
@@ -60,11 +60,19 @@
<evaluate expression="InitializeSubjectContext" />
<evaluate expression="SetSubjectFromAuthzCodeToResponseContext" />
<evaluate expression="ValidateScope" />
+ <evaluate expression="ValidateAudience" />
<evaluate expression="'proceed'" />
- <transition on="proceed" to="CheckAttributeResolutionForClient" />
+ <transition on="proceed" to="CheckTraditionalGrantForAudience" />
</action-state>
+ <!-- For standard grants, audience may or may not be a factor. -->
+ <decision-state id="CheckTraditionalGrantForAudience">
+ <if test="opensamlProfileRequestContext.getOutboundMessageContext().getSubcontext(T(net.shibboleth.idp.plugin.oidc.op.messaging.context.OIDCAuthenticationResponseContext)).getAudience().isEmpty()"
+ then="CheckAttributeResolutionForClient"
+ else="LookupAudienceMetadata" />
+ </decision-state>
+
<!-- These steps apply to grants that are self-contained on this endpoint. -->
<action-state id="ClientCredentialsGrantProcessing">
<evaluate expression="SetAuthenticationContextClassReferenceToResponseContext" />
@@ -77,7 +85,7 @@
<transition on="proceed" to="LookupAudienceMetadata" />
</action-state>
- <!-- For client credentials grant, need to flip the Relying Party here to the primary resource/audience. -->
+ <!-- May need to add a second Relying Party for the primary resource/audience. -->
<action-state id="LookupAudienceMetadata">
<evaluate expression="AudienceOIDCMetadataLookup" />
@@ -137,35 +145,10 @@
<transition on="proceed" to="DoConsentLookup" />
</action-state>
- <decision-state id="BuildResponse">
- <if test="NotClientCredentialsGrantCondition.test(opensamlProfileRequestContext)"
- then="TraditionalGrantResponse"
- else="ClientCredentialsGrantResponse" />
- </decision-state>
-
- <action-state id="TraditionalGrantResponse">
- <evaluate expression="PopulateIDTokenSignatureSigningParameters" />
- <evaluate expression="PopulateIDTokenEncryptionParameters" />
- <evaluate expression="SetOIDCAccessTokenToResponseContext" />
- <evaluate expression="SetRefreshTokenToResponseContext" />
- <evaluate expression="AddIDTokenShell" />
- <evaluate expression="AddAttributeClaimsToIDToken" />
- <evaluate expression="AddTokenDeliveryAttributesToIDToken" />
- <evaluate expression="AddAuthTimeToIDToken" />
- <evaluate expression="AddAcrToIDToken" />
- <evaluate expression="AddNonceToIDToken" />
- <evaluate expression="AddAccessTokenHashToIDToken" />
- <evaluate expression="SignIDToken" />
- <evaluate expression="EncryptIDToken" />
- <evaluate expression="'proceed'" />
-
- <transition on="proceed" to="PopulateOutboundInterceptContext" />
- </action-state>
-
<decision-state id="CheckAttributeResolutionForAudience">
<if test="ResolveAttributesForAudiencePredicate.test(opensamlProfileRequestContext)"
then="AttributeResolutionForAudience"
- else="ClientCredentialsGrantResponse" />
+ else="DoConsentLookup" />
</decision-state>
<action-state id="AttributeResolutionForAudience">
@@ -173,17 +156,77 @@
<evaluate expression="FilterAttributesForAudience" />
<evaluate expression="'proceed'" />
- <transition on="proceed" to="ClientCredentialsGrantResponse" />
+ <transition on="proceed" to="DoConsentLookup" />
+ </action-state>
+
+ <decision-state id="BuildResponse">
+ <if test="opensamlProfileRequestContext.getOutboundMessageContext().getSubcontext(T(net.shibboleth.idp.plugin.oidc.op.messaging.context.OIDCAuthenticationResponseContext)).getAudience().isEmpty()"
+ then="BuildTokensForUserInfoAccess"
+ else="BuildTokensForThirdPartyAccess" />
+ </decision-state>
+
+ <!--
+ Note no JWT encryption here. The token shouldn't even be a JWT, but even if it is,
+ the only audience is us, and the client can't be expected to decrypt it, so it
+ wouldn't be usable. If it were encrypted to our key then there would be no point to
+ allowing it to be a JWT.
+
+ Note also no attribute claims are added to the access token since that isn't a proper
+ delivery mechanism for claims to the OIDC client.
+ -->
+ <action-state id="BuildTokensForUserInfoAccess">
+ <evaluate expression="PopulateUserInfoAccessTokenSignatureSigningParameters" />
+ <evaluate expression="BuildOIDCAccessToken" />
+ <evaluate expression="SignOIDCAccessToken" />
+ <evaluate expression="SetOAuthAccessTokenToResponseContext" />
+ <evaluate expression="SetRefreshTokenToResponseContext" />
+ <evaluate expression="'proceed'" />
+
+ <transition on="proceed" to="ProducingIDToken" />
</action-state>
- <action-state id="ClientCredentialsGrantResponse">
- <evaluate expression="PopulateAccessTokenSignatureSigningParameters" />
- <evaluate expression="PopulateAccessTokenEncryptionParameters" />
+ <!--
+
+ This includes OIDC and OAuth use cases and all grant types but the primary audience
+ for the access token is the resource/audience. The OP MAY also be an audience for the
+ token if the "oidc" scope is in play, in which case the actions have to behave differently
+ or be disabled in some cases. Encryption is for example impossible in that case, as with
+ the OIDC-only use of JWTs above.
+ -->
+ <action-state id="BuildTokensForThirdPartyAccess">
+ <evaluate expression="PopulateThirdPartyAccessTokenSignatureSigningParameters" />
+ <evaluate expression="PopulateThirdPartyAccessTokenEncryptionParameters" />
<evaluate expression="AddAttributeClaimsToAccessToken" />
<evaluate expression="BuildAccessToken" />
<evaluate expression="SignAccessToken" />
<evaluate expression="EncryptAccessToken" />
<evaluate expression="SetOAuthAccessTokenToResponseContext" />
+ <evaluate expression="SetRefreshTokenToResponseContext" />
+ <evaluate expression="'proceed'" />
+
+ <transition on="proceed" to="ProducingIDToken" />
+ </action-state>
+
+ <!-- ID token only issued when scope includes "openid" -->
+
+ <decision-state id="ProducingIDToken">
+ <if test="IssueIDTokenCondition.test(opensamlProfileRequestContext)"
+ then="BuildIDToken"
+ else="PopulateOutboundInterceptContext" />
+ </decision-state>
+
+ <action-state id="BuildIDToken">
+ <evaluate expression="PopulateIDTokenSignatureSigningParameters" />
+ <evaluate expression="PopulateIDTokenEncryptionParameters" />
+ <evaluate expression="AddIDTokenShell" />
+ <evaluate expression="AddAttributeClaimsToIDToken" />
+ <evaluate expression="AddTokenDeliveryAttributesToIDToken" />
+ <evaluate expression="AddAuthTimeToIDToken" />
+ <evaluate expression="AddAcrToIDToken" />
+ <evaluate expression="AddNonceToIDToken" />
+ <evaluate expression="AddAccessTokenHashToIDToken" />
+ <evaluate expression="SignIDToken" />
+ <evaluate expression="EncryptIDToken" />
<evaluate expression="'proceed'" />
<transition on="proceed" to="PopulateOutboundInterceptContext" />
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 86ecda28..f7c36d64 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
@@ -23,20 +23,24 @@
</bean>
<bean id="InitializeOutboundMessageContext"
- class="net.shibboleth.idp.plugin.oidc.op.profile.impl.InitializeOutboundUserInfoResponseMessageContext"
+ class="net.shibboleth.idp.plugin.oidc.op.userinfo.profile.impl.InitializeOutboundUserInfoResponseMessageContext"
scope="prototype" />
- <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.oidc.RevocationCache" />
-
+ <!-- Used for metadata lookup. -->
<bean id="shibboleth.ClientIDLookupStrategy"
class="net.shibboleth.idp.plugin.oidc.op.profile.context.navigate.UserInfoRequestClientIDLookupFunction" />
- <bean id="shibboleth.UserInfoRequestClientIDLookupStrategy"
- class="net.shibboleth.idp.plugin.oidc.op.profile.context.navigate.UserInfoRequestClientIDLookupFunction" />
+ <bean id="ParseAccessToken"
+ class="net.shibboleth.idp.plugin.oidc.op.userinfo.profile.impl.ParseAccessToken" scope="prototype"
+ p:dataSealer-ref="#{'%{idp.oidc.tokenSealer:shibboleth.oidc.TokenSealer}'.trim()}"
+ p:credentialResolver-ref="SigningCredentialsResolver" />
+
+ <bean id="SigningCredentialsResolver" class="net.shibboleth.idp.relyingparty.impl.SigningCredentialsResolver"
+ c:_0-ref="shibboleth.RelyingPartyResolverService" />
+ <bean id="ValidateAccessToken"
+ class="net.shibboleth.idp.plugin.oidc.op.userinfo.profile.impl.ValidateAccessToken" scope="prototype" />
+
<bean id="ValidateScope" class="net.shibboleth.idp.plugin.oidc.op.profile.impl.ValidateScope" scope="prototype"
p:allowedScopeLookupStrategy="#{getObject('shibboleth.oidc.AllowedScopeStrategy') ?: getObject('shibboleth.oidc.DefaultAllowedScopeStrategy')}">
<property name="requestedScopeLookupStrategy">
@@ -117,8 +121,8 @@
<bean id="shibboleth.UserInfoResponseClaimsSetLookupStrategy"
class="net.shibboleth.idp.plugin.oidc.op.profile.context.navigate.UserInfoResponseClaimsSetLookupFunction" />
- <bean id="SignUserInfoResponse" class="net.shibboleth.idp.plugin.oidc.op.profile.impl.SignUserInfoResponse"
- scope="prototype">
+ <bean id="SignUserInfoResponse" class="net.shibboleth.idp.plugin.oidc.op.userinfo.profile.impl.SignUserInfoResponse"
+ scope="prototype">
<property name="securityParametersLookupStrategy">
<bean parent="shibboleth.Functions.Compose"
c:g-ref="shibboleth.ChildLookup.SecurityParameters"
@@ -130,10 +134,10 @@
scope="prototype" />
<bean id="FormOutboundMessage"
- class="net.shibboleth.idp.plugin.oidc.op.profile.impl.FormOutboundUserInfoResponseMessage" scope="prototype" />
+ class="net.shibboleth.idp.plugin.oidc.op.userinfo.profile.impl.FormOutboundUserInfoResponseMessage" scope="prototype" />
<bean id="BuildErrorResponseFromEvent"
- class="net.shibboleth.idp.plugin.oidc.op.profile.impl.BuildUserInfoErrorResponseFromEvent" scope="prototype"
+ class="net.shibboleth.idp.plugin.oidc.op.userinfo.profile.impl.BuildUserInfoErrorResponseFromEvent" 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/oidc/userinfo/userinfo-flow.xml b/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/userinfo/userinfo-flow.xml
index e22cf7bb..be5f41c5 100644
--- a/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/userinfo/userinfo-flow.xml
+++ b/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/userinfo/userinfo-flow.xml
@@ -15,8 +15,8 @@
<action-state id="DecodeMessage">
<evaluate expression="DecodeMessage" />
- <evaluate expression="ValidateAccessToken" />
<evaluate expression="PostDecodePopulateAuditContext" />
+ <evaluate expression="ParseAccessToken" />
<evaluate expression="'proceed'" />
<!-- DoMetadataLookup is expected to proceed to SelectConfiguration -->
@@ -44,6 +44,7 @@
</subflow-state>
<action-state id="OutboundContextsAndSecurityParameters">
+ <evaluate expression="ValidateAccessToken" />
<evaluate expression="ValidateScope" />
<evaluate expression="SetRequestedClaimsToResponseContext" />
<evaluate expression="SetTokenDeliveryAttributesFromTokenToResponseContext" />
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 e79e5955..455e4139 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
@@ -24,6 +24,7 @@
p:forcePKCE="%{idp.oidc.forcePKCE:false}"
p:allowPKCEPlain="%{idp.oidc.allowPKCEPlain:false}"
p:iDTokenLifetime="%{idp.oidc.idToken.defaultLifetime:PT1H}"
+ p:accessTokenType="%{idp.oauth2.accessToken.type:}"
p:accessTokenLifetime="%{idp.oidc.accessToken.defaultLifetime:PT10M}"
p:refreshTokenLifetime="%{idp.oidc.refreshToken.defaultLifetime:PT2H}"
p:alwaysIncludedAttributes="%{idp.oidc.alwaysIncludedAttributes:}" />
@@ -40,7 +41,8 @@
class="net.shibboleth.oidc.profile.config.OIDCUserInfoConfiguration"
p:issuer-ref="shibboleth.oidc.issuer"
p:encryptionOptional="%{idp.oidc.encryptionOptional:true}"
- p:deniedUserInfoAttributes="%{idp.oidc.deniedUserInfoAttributes:}" />
+ p:deniedUserInfoAttributes="%{idp.oidc.deniedUserInfoAttributes:}"
+ p:issuedClaimsValidator-ref="DefaultUserInfoJWTClaimsValidator" />
<bean id="OIDC.Registration" parent="AbstractOIDCProfile" lazy-init="true"
class="net.shibboleth.oidc.profile.config.OIDCDynamicRegistrationConfiguration"
@@ -214,6 +216,10 @@
<bean parent="shibboleth.MDDrivenDurationProperty" p:propertyName="iDTokenLifetime"
p:defaultValue="%{idp.oidc.idToken.defaultLifetime:PT1H}" />
</property>
+ <property name="accessTokenTypeLookupStrategy">
+ <bean parent="shibboleth.MDDrivenStringProperty" p:propertyName="accessTokenType"
+ p:defaultValue="%{idp.oidc.accessToken.type:}" />
+ </property>
<property name="accessTokenLifetimeLookupStrategy">
<bean parent="shibboleth.MDDrivenDurationProperty" p:propertyName="accessTokenLifetime"
p:defaultValue="%{idp.oidc.accessToken.defaultLifetime:PT10M}" />
@@ -309,6 +315,11 @@
</property>
</bean>
</property>
+ <property name="issuedClaimsValidatorLookupStrategy">
+ <bean parent="shibboleth.MDDrivenBeanProperty" p:propertyName="issuedClaimsValidator"
+ p:propertyType="#{T(net.shibboleth.oidc.jwt.claims.ClaimsValidator)}"
+ p:defaultValue-ref="DefaultUserInfoJWTClaimsValidator" />
+ </property>
</bean>
<bean id="OIDC.Registration.MDDriven" parent="AbstractMDDrivenOIDCFlowAwareProfile" lazy-init="true"
@@ -500,7 +511,7 @@
<ref bean="JWTIdentifierClaimsValidator" />
</util:list>
- <!-- Default issued JWT validation wiring (for introspection/revocation). -->
+ <!-- Default issued JWT validation wiring (for introspection/revocation/UserInfo). -->
<bean id="DefaultIntrospectionJWTClaimsValidator"
class="net.shibboleth.oidc.security.jwt.claims.impl.ChainingJWTClaimsValidator"
@@ -510,6 +521,10 @@
class="net.shibboleth.oidc.security.jwt.claims.impl.ChainingJWTClaimsValidator"
p:claimValidators-ref="RevocationClaimsValidators" />
+ <bean id="DefaultUserInfoJWTClaimsValidator"
+ class="net.shibboleth.oidc.security.jwt.claims.impl.ChainingJWTClaimsValidator"
+ p:claimValidators-ref="UserInfoClaimsValidators" />
+
<bean id="SelfIssuedClaimsValidator"
class="net.shibboleth.oidc.security.jwt.claims.impl.ExactMatchClaimsValidator"
p:claimName="iss">
@@ -575,6 +590,33 @@
</bean>
</util:list>
+ <bean id="OPInAudienceClaimsValidator"
+ class="net.shibboleth.oidc.security.jwt.claims.impl.AudienceClaimsValidator"
+ p:allowMissing="true">
+ <property name="audienceLookupStrategy">
+ <bean class="net.shibboleth.utilities.java.support.logic.BiFunctionSupport"
+ factory-method="forFunctionOfFirstArg"
+ c:_0-ref="shibboleth.ResponderIdLookup.Simple" />
+ </property>
+ </bean>
+
+ <util:list id="UserInfoClaimsValidators" value-type="net.shibboleth.oidc.jwt.claims.ClaimsValidator">
+ <ref bean="RequiredClaimsValidator" />
+ <ref bean="ExpiryClaimsValidator" />
+ <ref bean="NotBeforeClaimsValidator" />
+ <ref bean="SelfIssuedClaimsValidator" />
+ <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" />
+ </list>
+ </property>
+ </bean>
+ <ref bean="OPInAudienceClaimsValidator" />
+ <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.
diff --git a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/BuildAccessTokenTest.java b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/BuildAccessTokenTest.java
index d883801b..c4acd64c 100644
--- a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/BuildAccessTokenTest.java
+++ b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/BuildAccessTokenTest.java
@@ -21,6 +21,7 @@ import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertNotNull;
import static org.testng.Assert.assertTrue;
+import net.shibboleth.idp.authn.context.SubjectContext;
import net.shibboleth.idp.plugin.oidc.op.messaging.context.AccessTokenContext;
import net.shibboleth.idp.plugin.oidc.op.profile.impl.BaseOIDCResponseActionTest;
import net.shibboleth.idp.plugin.oidc.op.token.support.AccessTokenClaimsSet;
@@ -57,12 +58,14 @@ import com.nimbusds.oauth2.sdk.id.ClientID;
public class BuildAccessTokenTest extends BaseOIDCResponseActionTest {
/** Action to test. */
- private BuildAccessToken action;
+ @Nullable private BuildAccessToken action;
@BeforeMethod
protected void setUp() throws Exception {
super.setUp();
+ profileRequestCtx.getSubcontext(SubjectContext.class, true).setPrincipalName("jdoe");
+
respCtx.setAuthTime(Instant.now());
respCtx.setSubject(clientId);
respCtx.setAcr("0");
@@ -142,9 +145,8 @@ public class BuildAccessTokenTest extends BaseOIDCResponseActionTest {
throws ComponentInitializationException, NoSuchAlgorithmException {
if ("JWT".equals(type)) {
action.setAccessTokenTypeLookupStrategy(FunctionSupport.constant("JWT"));
- } else if (type == null) {
- action.setDataSealer(getDataSealer());
}
+ action.setDataSealer(getDataSealer());
action.setClientIDLookupStrategy(FunctionSupport.constant(new ClientID(clientId)));
action.initialize();
}
@@ -179,6 +181,7 @@ public class BuildAccessTokenTest extends BaseOIDCResponseActionTest {
assertTrue(at.getIssuedAt().isBefore(Instant.now()));
assertEquals(at.getScope(), scope);
assertEquals(at.getSubject(), clientId);
+ assertEquals(at.getPrincipal(), "jdoe");
} else if (ctx.getJWT() != null) {
final JWTClaimsSet claims = ctx.getJWT().getJWTClaimsSet();
assertNotNull(claims);
@@ -191,6 +194,10 @@ public class BuildAccessTokenTest extends BaseOIDCResponseActionTest {
assertTrue(claims.getIssueTime().toInstant().isBefore(Instant.now()));
assertEquals(claims.getStringClaim(TokenClaimsSet.KEY_SCOPE), scope.toString());
assertEquals(claims.getSubject(), clientId);
+
+ final JWTClaimsSet unsealedClaims =
+ JWTClaimsSet.parse(getDataSealer().unwrap(claims.getStringClaim(TokenClaimsSet.KEY_SEALED_FOR_OP)));
+ assertEquals(unsealedClaims.getStringClaim(TokenClaimsSet.KEY_USER_PRINCIPAL), "jdoe");
} else {
throw new RuntimeException("No token found");
}
diff --git a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/AbstractOidcFlowTest.java b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/AbstractOidcFlowTest.java
index b5a6b294..be7781d1 100644
--- a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/AbstractOidcFlowTest.java
+++ b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/AbstractOidcFlowTest.java
@@ -243,7 +243,7 @@ public abstract class AbstractOidcFlowTest extends AbstractFlowTest {
metadata.setIDTokenJWEEnc(encMethod);
metadata.setTokenEndpointAuthMethod(tokenEndpointMethod);
metadata.setUserInfoJWSAlg(userInfoSigAlg);
- metadata.setCustomField("audience", List.of("https://rp.example.org", "https://rp2.example.org"));
+ metadata.setCustomField("audience", List.of("https://rp.example.org", "https://rp2.example.org", "https://resource.example.org"));
final OIDCClientInformation information;
if (publicKey == null) {
information = new OIDCClientInformation(new ClientID(clientId), new Date(),
diff --git a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/AuthorizeFlowTest.java b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/AuthorizeFlowTest.java
index 62bb2070..13fb2f9d 100644
--- a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/AuthorizeFlowTest.java
+++ b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/AuthorizeFlowTest.java
@@ -18,9 +18,12 @@
package net.shibboleth.idp.plugin.oidc.op.profile.flow;
import java.io.IOException;
+import java.text.ParseException;
import java.time.Duration;
import java.time.Instant;
+import java.util.Collections;
import java.util.Date;
+import java.util.List;
import org.opensaml.storage.StorageService;
import org.springframework.beans.factory.annotation.Autowired;
@@ -36,13 +39,13 @@ import com.nimbusds.jwt.JWT;
import com.nimbusds.jwt.JWTClaimsSet;
import com.nimbusds.jwt.PlainJWT;
import com.nimbusds.jwt.SignedJWT;
-import com.nimbusds.oauth2.sdk.ParseException;
import com.nimbusds.oauth2.sdk.Scope;
import com.nimbusds.openid.connect.sdk.AuthenticationResponse;
import com.nimbusds.openid.connect.sdk.AuthenticationSuccessResponse;
import com.nimbusds.openid.connect.sdk.claims.ClaimRequirement;
import com.nimbusds.openid.connect.sdk.claims.ClaimsSetRequest;
+import net.shibboleth.idp.plugin.oidc.op.token.support.AccessTokenClaimsSet;
import net.shibboleth.idp.plugin.oidc.op.token.support.AuthorizeCodeClaimsSet;
import net.shibboleth.idp.session.SessionException;
import net.shibboleth.oidc.profile.core.OidcError;
@@ -55,6 +58,8 @@ public class AuthorizeFlowTest extends AbstractOidcFlowTest {
public static final String FLOW_ID = "oidc/authorize";
+ String resource = "https://resource.example.org";
+ String issuer = "https://op.example.org";
String redirectUri = "https://example.org/cb";
String clientId = "mockClientId";
String clientSecret = "mockClientSecretmockClientSecretmockClientSecretmockClientSecretmockClientSecret";
@@ -74,7 +79,7 @@ public class AuthorizeFlowTest extends AbstractOidcFlowTest {
}
@Test
- public void testWithAuthorizationCodeFlow() throws IOException, ParseException, SessionException {
+ public void testWithAuthorizationCodeFlow() throws IOException, SessionException {
request.setMethod("GET");
request.setQueryString("client_id=mockClientId&response_type=code&scope=openid%20profile&redirect_uri="
+ redirectUri);
@@ -92,7 +97,25 @@ public class AuthorizeFlowTest extends AbstractOidcFlowTest {
}
@Test
- public void testWithAuthorizationCodeFlowNoOpenid() throws IOException, ParseException, SessionException {
+ public void testWithAuthorizationCodeFlowAndResource() throws IOException, SessionException {
+ request.setMethod("GET");
+ request.setQueryString("client_id=mockClientId&response_type=code&scope=openid%20profile&resource=" + resource +
+ "&redirect_uri=" + redirectUri);
+ storeMetadata(storageService, clientId, clientSecret, scope, redirectUri);
+
+ initializeThreadLocals();
+
+ final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+ final AuthenticationResponse responseMessage = parseSuccessResponse(result, AuthenticationResponse.class);
+ final AuthenticationSuccessResponse successResponse = responseMessage.toSuccessResponse();
+ Assert.assertEquals(successResponse.getRedirectionURI().toString(), redirectUri);
+ Assert.assertNull(successResponse.getIDToken());
+ Assert.assertNull(successResponse.getAccessToken());
+ Assert.assertNotNull(successResponse.getAuthorizationCode());
+ }
+
+ @Test
+ public void testWithAuthorizationCodeFlowNoOpenid() throws IOException, SessionException {
request.setMethod("GET");
request.setQueryString("client_id=mockClientId&response_type=code&scope=profile&redirect_uri="
+ redirectUri);
@@ -105,7 +128,7 @@ public class AuthorizeFlowTest extends AbstractOidcFlowTest {
}
@Test
- public void testWithImplicitFlowNoNonce() throws IOException, ParseException, SessionException {
+ public void testWithImplicitFlowNoNonce() throws IOException, SessionException {
request.setMethod("GET");
request.setQueryString("client_id=mockClientId&response_type=id_token&scope=openid%20profile&redirect_uri="
+ redirectUri);
@@ -118,7 +141,7 @@ public class AuthorizeFlowTest extends AbstractOidcFlowTest {
}
@Test
- public void testWithImplicitFlow() throws IOException, ParseException, SessionException {
+ public void testWithImplicitFlow() throws IOException, SessionException {
request.setMethod("GET");
request.setQueryString("client_id=mockClientId&response_type=id_token&scope=openid%20profile&redirect_uri="
+ redirectUri + "&nonce=idhas3h23hi13h1o2i32");
@@ -136,7 +159,25 @@ public class AuthorizeFlowTest extends AbstractOidcFlowTest {
}
@Test
- public void testWithImplicitTokenFlow() throws IOException, ParseException, SessionException {
+ public void testWithImplicitFlowAndResource() throws IOException, SessionException {
+ request.setMethod("GET");
+ request.setQueryString("client_id=mockClientId&response_type=id_token&scope=openid%20profile&resource=" + resource
+ + "&redirect_uri=" + redirectUri + "&nonce=idhas3h23hi13h1o2i32");
+ storeMetadata(storageService, clientId, clientSecret, scope, redirectUri);
+
+ initializeThreadLocals();
+
+ final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+ final AuthenticationResponse responseMessage = parseSuccessResponse(result, AuthenticationResponse.class);
+ final AuthenticationSuccessResponse successResponse = responseMessage.toSuccessResponse();
+ Assert.assertEquals(successResponse.getRedirectionURI().toString(), redirectUri);
+ Assert.assertNotNull(successResponse.getIDToken());
+ Assert.assertNull(successResponse.getAccessToken());
+ Assert.assertNull(successResponse.getAuthorizationCode());
+ }
+
+ @Test
+ public void testWithImplicitTokenFlow() throws IOException, SessionException, DataSealerException, ParseException {
request.setMethod("GET");
request.setQueryString("client_id=mockClientId&response_type=id_token+token&scope=openid%20profile"
+ "&redirect_uri="+ redirectUri + "&nonce=idhas3h23hi13h1o2i32");
@@ -151,10 +192,37 @@ public class AuthorizeFlowTest extends AbstractOidcFlowTest {
Assert.assertNotNull(successResponse.getIDToken());
Assert.assertNotNull(successResponse.getAccessToken());
Assert.assertNull(successResponse.getAuthorizationCode());
+
+ final AccessTokenClaimsSet token =
+ AccessTokenClaimsSet.parse(successResponse.getAccessToken().getValue(), getDataSealer());
+ Assert.assertEquals(token.getAudience(), Collections.singletonList(issuer));
+ Assert.assertNull(token.getClaimsSet().getStringClaim("eduPersonScopedAffiliation"));
}
@Test
- public void testWithImplicitTokenFlowNoNonce() throws IOException, ParseException, SessionException {
+ public void testWithImplicitTokenFlowAndResource() throws IOException, SessionException, ParseException {
+ request.setMethod("GET");
+ request.setQueryString("client_id=mockClientId&response_type=id_token+token&scope=openid%20profile"
+ + "&resource=" + resource + "&redirect_uri="+ redirectUri + "&nonce=idhas3h23hi13h1o2i32");
+ storeMetadata(storageService, clientId, clientSecret, scope, redirectUri);
+
+ initializeThreadLocals();
+
+ final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+ final AuthenticationResponse responseMessage = parseSuccessResponse(result, AuthenticationResponse.class);
+ final AuthenticationSuccessResponse successResponse = responseMessage.toSuccessResponse();
+ Assert.assertEquals(successResponse.getRedirectionURI().toString(), redirectUri);
+ Assert.assertNotNull(successResponse.getIDToken());
+ Assert.assertNotNull(successResponse.getAccessToken());
+ Assert.assertNull(successResponse.getAuthorizationCode());
+
+ final JWTClaimsSet token = SignedJWT.parse(successResponse.getAccessToken().getValue()).getJWTClaimsSet();
+ Assert.assertEquals(token.getAudience(), List.of(resource, issuer));
+ Assert.assertNotNull(token.getStringClaim("eduPersonScopedAffiliation"));
+ }
+
+ @Test
+ public void testWithImplicitTokenFlowNoNonce() throws IOException, SessionException {
request.setMethod("GET");
request.setQueryString("client_id=mockClientId&response_type=id_token+token&scope=openid%20profile"
+ "&redirect_uri="+ redirectUri);
@@ -167,7 +235,7 @@ public class AuthorizeFlowTest extends AbstractOidcFlowTest {
}
@Test
- public void testWithImplicitFlowNoOpenIdScope() throws IOException, ParseException, SessionException {
+ public void testWithImplicitFlowNoOpenIdScope() throws IOException, SessionException {
request.setMethod("GET");
request.setQueryString("client_id=mockClientId&response_type=id_token&scope=profile&redirect_uri="
+ redirectUri + "&nonce=idhas3h23hi13h1o2i32");
@@ -180,7 +248,7 @@ public class AuthorizeFlowTest extends AbstractOidcFlowTest {
}
@Test
- public void testWithHybridIdTokenFlow() throws IOException, ParseException, SessionException {
+ public void testWithHybridIdTokenFlow() throws IOException, SessionException {
request.setMethod("GET");
request.setQueryString("client_id=mockClientId&response_type=code+id_token&scope=openid%20profile"
+ "&redirect_uri=" + redirectUri + "&nonce=idhas3h23hi13h1o2i32");
@@ -198,7 +266,25 @@ public class AuthorizeFlowTest extends AbstractOidcFlowTest {
}
@Test
- public void testWithHybridIdTokenFlowNoNonce() throws IOException, ParseException, SessionException {
+ public void testWithHybridIdTokenFlowAndResource() throws IOException, SessionException {
+ request.setMethod("GET");
+ request.setQueryString("client_id=mockClientId&response_type=code+id_token&scope=openid%20profile"
+ + "&resource=" + resource + "&redirect_uri=" + redirectUri + "&nonce=idhas3h23hi13h1o2i32");
+ storeMetadata(storageService, clientId, clientSecret, scope, redirectUri);
+
+ initializeThreadLocals();
+
+ final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+ final AuthenticationResponse responseMessage = parseSuccessResponse(result, AuthenticationResponse.class);
+ final AuthenticationSuccessResponse successResponse = responseMessage.toSuccessResponse();
+ Assert.assertEquals(successResponse.getRedirectionURI().toString(), redirectUri);
+ Assert.assertNotNull(successResponse.getIDToken());
+ Assert.assertNull(successResponse.getAccessToken());
+ Assert.assertNotNull(successResponse.getAuthorizationCode());
+ }
+
+ @Test
+ public void testWithHybridIdTokenFlowNoNonce() throws IOException, SessionException {
request.setMethod("GET");
request.setQueryString("client_id=mockClientId&response_type=code+id_token&scope=openid%20profile"
+ "&redirect_uri=" + redirectUri);
@@ -211,7 +297,7 @@ public class AuthorizeFlowTest extends AbstractOidcFlowTest {
}
@Test
- public void testWithHybridTokenFlow() throws IOException, ParseException, SessionException {
+ public void testWithHybridTokenFlow() throws IOException, SessionException, ParseException, DataSealerException {
request.setMethod("GET");
request.setQueryString("client_id=mockClientId&response_type=code+token&scope=openid%20profile"
+ "&redirect_uri="+ redirectUri);
@@ -227,10 +313,38 @@ public class AuthorizeFlowTest extends AbstractOidcFlowTest {
Assert.assertNull(successResponse.getIDToken());
Assert.assertNotNull(successResponse.getAccessToken());
Assert.assertNotNull(successResponse.getAuthorizationCode());
+
+ final AccessTokenClaimsSet token =
+ AccessTokenClaimsSet.parse(successResponse.getAccessToken().getValue(), getDataSealer());
+ Assert.assertEquals(token.getAudience(), Collections.singletonList(issuer));
+ Assert.assertNull(token.getClaimsSet().getStringClaim("eduPersonScopedAffiliation"));
}
@Test
- public void testWithHybridIdTokenTokenFlow() throws IOException, ParseException, SessionException {
+ public void testWithHybridTokenFlowAndResource() throws IOException, SessionException, ParseException {
+ request.setMethod("GET");
+ request.setQueryString("client_id=mockClientId&response_type=code+token&scope=openid%20profile"
+ + "&resource=" + resource + "&redirect_uri="+ redirectUri);
+ storeMetadata(storageService, clientId, clientSecret, scope, redirectUri);
+
+ initializeThreadLocals();
+
+ // success response as id_token is not involved and thus nonce is not required
+ final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+ final AuthenticationResponse responseMessage = parseSuccessResponse(result, AuthenticationResponse.class);
+ final AuthenticationSuccessResponse successResponse = responseMessage.toSuccessResponse();
+ Assert.assertEquals(successResponse.getRedirectionURI().toString(), redirectUri);
+ Assert.assertNull(successResponse.getIDToken());
+ Assert.assertNotNull(successResponse.getAccessToken());
+ Assert.assertNotNull(successResponse.getAuthorizationCode());
+
+ final JWTClaimsSet token = SignedJWT.parse(successResponse.getAccessToken().getValue()).getJWTClaimsSet();
+ Assert.assertEquals(token.getAudience(), List.of(resource, issuer));
+ Assert.assertNotNull(token.getStringClaim("eduPersonScopedAffiliation"));
+ }
+
+ @Test
+ public void testWithHybridIdTokenTokenFlow() throws IOException, SessionException, ParseException, DataSealerException {
request.setMethod("GET");
request.setQueryString("client_id=mockClientId&response_type=code+id_token+token&scope=openid%20profile"
+ "&redirect_uri="+ redirectUri + "&nonce=idhas3h23hi13h1o2i32");
@@ -245,10 +359,37 @@ public class AuthorizeFlowTest extends AbstractOidcFlowTest {
Assert.assertNotNull(successResponse.getIDToken());
Assert.assertNotNull(successResponse.getAccessToken());
Assert.assertNotNull(successResponse.getAuthorizationCode());
+
+ final AccessTokenClaimsSet token =
+ AccessTokenClaimsSet.parse(successResponse.getAccessToken().getValue(), getDataSealer());
+ Assert.assertEquals(token.getAudience(), Collections.singletonList(issuer));
+ Assert.assertNull(token.getClaimsSet().getStringClaim("eduPersonScopedAffiliation"));
+ }
+
+ @Test
+ public void testWithHybridIdTokenTokenFlowAndResource() throws IOException, SessionException, ParseException {
+ request.setMethod("GET");
+ request.setQueryString("client_id=mockClientId&response_type=code+id_token+token&scope=openid%20profile"
+ + "&resource=" + resource + "&redirect_uri="+ redirectUri + "&nonce=idhas3h23hi13h1o2i32");
+ storeMetadata(storageService, clientId, clientSecret, scope, redirectUri);
+
+ initializeThreadLocals();
+
+ final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+ final AuthenticationResponse responseMessage = parseSuccessResponse(result, AuthenticationResponse.class);
+ final AuthenticationSuccessResponse successResponse = responseMessage.toSuccessResponse();
+ Assert.assertEquals(successResponse.getRedirectionURI().toString(), redirectUri);
+ Assert.assertNotNull(successResponse.getIDToken());
+ Assert.assertNotNull(successResponse.getAccessToken());
+ Assert.assertNotNull(successResponse.getAuthorizationCode());
+
+ final JWTClaimsSet token = SignedJWT.parse(successResponse.getAccessToken().getValue()).getJWTClaimsSet();
+ Assert.assertEquals(token.getAudience(), List.of(resource, issuer));
+ Assert.assertNotNull(token.getStringClaim("eduPersonScopedAffiliation"));
}
@Test
- public void testWithHybridIdTokenTokenFlowNoNonce() throws IOException, ParseException, SessionException {
+ public void testWithHybridIdTokenTokenFlowNoNonce() throws IOException, SessionException {
request.setMethod("GET");
request.setQueryString("client_id=mockClientId&response_type=code+id_token+token&scope=openid%20profile"
+ "&redirect_uri="+ redirectUri);
@@ -261,7 +402,7 @@ public class AuthorizeFlowTest extends AbstractOidcFlowTest {
}
@Test
- public void testWithAuthorizationCodeFlowUnforcedPKCE() throws IOException, ParseException, SessionException {
+ public void testWithAuthorizationCodeFlowUnforcedPKCE() throws IOException, SessionException {
request.setMethod("GET");
request.setQueryString("client_id=mockClientIdPKCEPlainUnforced&response_type=code&scope=openid%20profile&redirect_uri="
+ redirectUri);
@@ -279,7 +420,7 @@ public class AuthorizeFlowTest extends AbstractOidcFlowTest {
}
@Test
- public void testWithAuthorizationCodeFlowForcedPlainPKCEMissingChallenge() throws IOException, ParseException, SessionException {
+ public void testWithAuthorizationCodeFlowForcedPlainPKCEMissingChallenge() throws IOException, SessionException {
request.setMethod("GET");
request.setQueryString("client_id=mockClientIdPKCEPlain&response_type=code&scope=openid%20profile&redirect_uri="
+ redirectUri);
@@ -293,7 +434,7 @@ public class AuthorizeFlowTest extends AbstractOidcFlowTest {
}
@Test
- public void testWithAuthorizationCodeFlowForcedPlainPKCEUnknownChallenge() throws IOException, ParseException, SessionException {
+ public void testWithAuthorizationCodeFlowForcedPlainPKCEUnknownChallenge() throws IOException, SessionException {
request.setMethod("GET");
request.setQueryString("client_id=mockClientIdPKCEPlain&response_type=code&scope=openid%20profile&redirect_uri="
+ redirectUri + "&code_challenge=osdfojsfod&code_challenge_method=unsupported");
@@ -307,7 +448,7 @@ public class AuthorizeFlowTest extends AbstractOidcFlowTest {
}
@Test
- public void testWithAuthorizationCodeFlowForcedPlainPKCEValidChallenge() throws IOException, ParseException, SessionException {
+ public void testWithAuthorizationCodeFlowForcedPlainPKCEValidChallenge() throws IOException, SessionException {
request.setMethod("GET");
request.setQueryString("client_id=mockClientIdPKCEPlain&response_type=code&scope=openid%20profile&redirect_uri="
+ redirectUri + "&code_challenge=osdfojsfod&code_challenge_method=plain");
@@ -325,7 +466,7 @@ public class AuthorizeFlowTest extends AbstractOidcFlowTest {
}
@Test
- public void testWithAuthorizationCodeFlowForcedS256PKCEPlainChallenge() throws IOException, ParseException, SessionException {
+ public void testWithAuthorizationCodeFlowForcedS256PKCEPlainChallenge() throws IOException, SessionException {
request.setMethod("GET");
request.setQueryString("client_id=mockClientIdPKCES256&response_type=code&scope=openid%20profile&redirect_uri="
+ redirectUri + "&code_challenge=osdfojsfod&code_challenge_method=plain");
@@ -339,7 +480,7 @@ public class AuthorizeFlowTest extends AbstractOidcFlowTest {
}
@Test
- public void testWithAuthorizationCodeFlowForcedS256PKCEUnknownChallenge() throws IOException, ParseException, SessionException {
+ public void testWithAuthorizationCodeFlowForcedS256PKCEUnknownChallenge() throws IOException, SessionException {
request.setMethod("GET");
request.setQueryString("client_id=mockClientIdPKCES256&response_type=code&scope=openid%20profile&redirect_uri="
+ redirectUri + "&code_challenge=osdfojsfod&code_challenge_method=unknown");
@@ -353,7 +494,7 @@ public class AuthorizeFlowTest extends AbstractOidcFlowTest {
}
@Test
- public void testWithAuthorizationCodeFlowForcedS256PKCEValidChallenge() throws IOException, ParseException, SessionException {
+ public void testWithAuthorizationCodeFlowForcedS256PKCEValidChallenge() throws IOException, SessionException {
request.setMethod("GET");
request.setQueryString("client_id=mockClientIdPKCES256&response_type=code&scope=openid%20profile&redirect_uri="
+ redirectUri + "&code_challenge=osdfojsfod&code_challenge_method=S256");
@@ -371,7 +512,7 @@ public class AuthorizeFlowTest extends AbstractOidcFlowTest {
}
@Test
- public void testWithAuthorizationCodeFlowNoScopes() throws IOException, ParseException, SessionException {
+ public void testWithAuthorizationCodeFlowNoScopes() throws IOException, SessionException {
request.setMethod("GET");
request.setQueryString("client_id=mockClientId&response_type=code&scope=openid%20profile&redirect_uri="
+ redirectUri);
@@ -387,7 +528,7 @@ public class AuthorizeFlowTest extends AbstractOidcFlowTest {
}
@Test
- public void testWithAuthorizationCodeFlowWithIDTokenClaims() throws IOException, ParseException, SessionException, java.text.ParseException, DataSealerException {
+ public void testWithAuthorizationCodeFlowWithIDTokenClaims() throws IOException, SessionException, DataSealerException, ParseException {
request.setMethod("GET");
request.setQueryString("client_id=mockClientId&response_type=code&scope=openid%20profile"
+ "&claims=%7B%22id_token%22%3A%7B%22email%22%3A%7B%22essential%22%3Atrue%7D%7D%7D"
@@ -416,7 +557,7 @@ public class AuthorizeFlowTest extends AbstractOidcFlowTest {
}
@Test
- public void testWithAuthorizationCodeFlowWithUIClaims() throws IOException, ParseException, SessionException, java.text.ParseException, DataSealerException {
+ public void testWithAuthorizationCodeFlowWithUIClaims() throws IOException, SessionException, DataSealerException, ParseException {
request.setMethod("GET");
request.setQueryString("client_id=mockClientId&response_type=code&scope=openid%20profile"
+ "&claims=%7B%22userinfo%22%3A%7B%22email%22%3A%7B%22essential%22%3Atrue%7D%7D%7D"
@@ -445,7 +586,7 @@ public class AuthorizeFlowTest extends AbstractOidcFlowTest {
}
@Test
- public void testWithAuthorizationCodeFlowUsingSAMLMetadata() throws IOException, ParseException, SessionException {
+ public void testWithAuthorizationCodeFlowUsingSAMLMetadata() throws IOException, SessionException {
request.setMethod("GET");
request.setQueryString("client_id=mockSamlClientId&response_type=code&scope=openid%20profile&redirect_uri="
+ redirectUri);
@@ -463,7 +604,7 @@ public class AuthorizeFlowTest extends AbstractOidcFlowTest {
}
@Test
- public void testWithAuthorizationCodeFlowUsingUntrustedRP() throws IOException, ParseException, SessionException {
+ public void testWithAuthorizationCodeFlowUsingUntrustedRP() throws IOException, SessionException {
request.setMethod("GET");
request.setQueryString("client_id=notTrusted&response_type=code&scope=openid%20profile&redirect_uri="
+ redirectUri);
@@ -475,7 +616,7 @@ public class AuthorizeFlowTest extends AbstractOidcFlowTest {
}
@Test
- public void testWithPlainReqObjectExpired() throws IOException, ParseException, SessionException {
+ public void testWithPlainReqObjectExpired() throws IOException, SessionException {
final JWTClaimsSet ro = new JWTClaimsSet.Builder()
.expirationTime(Date.from(Instant.now().minus(Duration.ofMinutes(5))))
.build();
@@ -483,7 +624,7 @@ public class AuthorizeFlowTest extends AbstractOidcFlowTest {
}
@Test
- public void testWithPlainReqObjectNbfInFuture() throws IOException, ParseException, SessionException {
+ public void testWithPlainReqObjectNbfInFuture() throws IOException, SessionException {
final JWTClaimsSet ro = new JWTClaimsSet.Builder()
.notBeforeTime(Date.from(Instant.now().plus(Duration.ofMinutes(5))))
.build();
@@ -491,7 +632,7 @@ public class AuthorizeFlowTest extends AbstractOidcFlowTest {
}
@Test
- public void testWithPlainReqObjectOverwriteRedirectUri() throws IOException, ParseException, SessionException {
+ public void testWithPlainReqObjectOverwriteRedirectUri() throws IOException, SessionException {
final JWTClaimsSet ro = new JWTClaimsSet.Builder()
.claim("redirect_uri", redirectUri)
.build();
@@ -513,8 +654,8 @@ public class AuthorizeFlowTest extends AbstractOidcFlowTest {
}
@Test
- public void testWithPlainReqObjectClaimsRequest() throws IOException, ParseException, SessionException,
- java.text.ParseException, DataSealerException {
+ public void testWithPlainReqObjectClaimsRequest() throws IOException, SessionException,
+ DataSealerException, ParseException {
final String payload = "{\n"
+ " \"iss\": \"" + clientId + "\",\n"
+ " \"response_type\": \"code\",\n"
@@ -578,16 +719,16 @@ public class AuthorizeFlowTest extends AbstractOidcFlowTest {
}
@Test
- public void testWithSignedReqObjectNoIssuer() throws IOException, ParseException, SessionException,
+ public void testWithSignedReqObjectNoIssuer() throws IOException, SessionException,
JOSEException {
final JWTClaimsSet ro = new JWTClaimsSet.Builder()
- .audience("https://op.example.org")
+ .audience(issuer)
.build();
assertRequestObjectError(createSecretJWT(ro, clientSecret));
}
@Test
- public void testWithSignedReqObjectNoAudience() throws IOException, ParseException, SessionException,
+ public void testWithSignedReqObjectNoAudience() throws IOException, SessionException,
JOSEException {
final JWTClaimsSet ro = new JWTClaimsSet.Builder()
.issuer(clientId)
@@ -596,17 +737,17 @@ public class AuthorizeFlowTest extends AbstractOidcFlowTest {
}
@Test
- public void testWithSignedReqObjectWrongIssuer() throws IOException, ParseException, SessionException,
+ public void testWithSignedReqObjectWrongIssuer() throws IOException, SessionException,
JOSEException {
final JWTClaimsSet ro = new JWTClaimsSet.Builder()
- .audience("https://op.example.org")
+ .audience(issuer)
.issuer("invalid")
.build();
assertRequestObjectError(createSecretJWT(ro, clientSecret));
}
@Test
- public void testWithSignedReqObjectWrongAudience() throws IOException, ParseException, SessionException,
+ public void testWithSignedReqObjectWrongAudience() throws IOException, SessionException,
JOSEException {
final JWTClaimsSet ro = new JWTClaimsSet.Builder()
.audience("https://invalid.org")
@@ -619,7 +760,7 @@ public class AuthorizeFlowTest extends AbstractOidcFlowTest {
public void testWithSignedReqObjectOverwriteRedirectUri() throws IOException, ParseException,
SessionException, JOSEException {
final JWTClaimsSet ro = new JWTClaimsSet.Builder()
- .audience("https://op.example.org")
+ .audience(issuer)
.issuer(clientId)
.claim("redirect_uri", redirectUri)
.build();
@@ -641,8 +782,8 @@ public class AuthorizeFlowTest extends AbstractOidcFlowTest {
}
@Test
- public void testWithSignedReqObjectClaimsRequest() throws IOException, ParseException,
- SessionException, JOSEException, java.text.ParseException, DataSealerException {
+ public void testWithSignedReqObjectClaimsRequest() throws IOException,
+ SessionException, JOSEException, DataSealerException, ParseException {
final String payload = "{\n"
+ " \"iss\": \"" + clientId + "\",\n"
+ " \"response_type\": \"code\",\n"
diff --git a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/TokenFlowTest.java b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/TokenFlowTest.java
index 01fd363e..64cbe96c 100644
--- a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/TokenFlowTest.java
+++ b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/TokenFlowTest.java
@@ -129,7 +129,12 @@ public class TokenFlowTest extends AbstractOidcClientAuthenticationFlowTest {
assertErrorCode(result, OAuth2Error.INVALID_GRANT_CODE);
}
- @Test
+ /**
+ * TODO: This test "fails" now because it's honoring a non-OIDC request by assuming there has to be
+ * a requested and allowed audience/resource. The original success outcome was an anomaly due to the
+ * original grant handling not supporting the audience notion.
+ */
+ @Test(enabled=false)
public void testNoScopes() throws Exception {
setHttpFormRequest("POST",
createRequestParameters(redirectUri, "authorization_code",
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 6eb852d5..f2e0ff6a 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
@@ -91,15 +91,15 @@ public abstract class BaseOIDCResponseActionTest extends OpenSAMLInitBaseTestCas
protected ProfileRequestContext profileRequestCtx;
- Credential credentialRSA;
+ protected Credential credentialRSA;
- Credential credentialEC256;
+ protected Credential credentialEC256;
- Credential credentialEC384;
+ protected Credential credentialEC384;
- Credential credentialEC521;
+ protected Credential credentialEC521;
- Credential credentialHMAC;
+ protected Credential credentialHMAC;
public BaseOIDCResponseActionTest() {
subject = "generatedSubject";
diff --git a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/SetAccessTokenToResponseContextTest.java b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/SetAccessTokenToResponseContextTest.java
deleted file mode 100644
index e6fa6333..00000000
--- a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/SetAccessTokenToResponseContextTest.java
+++ /dev/null
@@ -1,283 +0,0 @@
-/*
- * Licensed to the University Corporation for Advanced Internet Development,
- * Inc. (UCAID) under one or more contributor license agreements. See the
- * NOTICE file distributed with this work for additional information regarding
- * copyright ownership. The UCAID licenses this file to You under the Apache
- * License, Version 2.0 (the "License"); you may not use this file except in
- * compliance with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package net.shibboleth.idp.plugin.oidc.op.profile.impl;
-
-import net.shibboleth.idp.authn.context.SubjectContext;
-import net.shibboleth.idp.plugin.oidc.op.messaging.context.OIDCAuthenticationResponseConsentContext;
-import net.shibboleth.idp.plugin.oidc.op.messaging.context.OIDCAuthenticationResponseTokenClaimsContext;
-import net.shibboleth.idp.plugin.oidc.op.token.support.AccessTokenClaimsSet;
-import net.shibboleth.idp.plugin.oidc.op.token.support.AuthorizeCodeClaimsSet;
-import net.shibboleth.idp.plugin.oidc.op.token.support.TokenClaimsSet;
-import net.shibboleth.idp.profile.IdPEventIds;
-import net.shibboleth.idp.profile.context.RelyingPartyContext;
-import net.shibboleth.idp.profile.testing.ActionTestingSupport;
-import net.shibboleth.utilities.java.support.component.ComponentInitializationException;
-import net.shibboleth.utilities.java.support.security.DataSealerException;
-
-import java.net.URI;
-import java.net.URISyntaxException;
-import java.security.NoSuchAlgorithmException;
-import java.text.ParseException;
-import java.time.Instant;
-
-import org.opensaml.profile.action.EventIds;
-import org.springframework.webflow.execution.Event;
-import org.testng.Assert;
-import org.testng.annotations.Test;
-
-import com.nimbusds.oauth2.sdk.RefreshTokenGrant;
-import com.nimbusds.oauth2.sdk.Scope;
-import com.nimbusds.oauth2.sdk.TokenRequest;
-import com.nimbusds.oauth2.sdk.id.ClientID;
-import com.nimbusds.oauth2.sdk.token.RefreshToken;
-import com.nimbusds.openid.connect.sdk.claims.ACR;
-
-// Checkstyle: ThrowsCount OFF
-
-/** {@link SetAccessTokenToResponseContext} unit test. */
-public class SetAccessTokenToResponseContextTest extends BaseOIDCResponseActionTest {
-
- /** Action to test. */
- private SetAccessTokenToResponseContext action;
-
- private void init() throws ComponentInitializationException, NoSuchAlgorithmException, URISyntaxException {
- respCtx.setScope(new Scope());
- final TokenClaimsSet claims = new AuthorizeCodeClaimsSet.Builder()
- .setJWTID(idGenerator)
- .setClientID(new ClientID())
- .setIssuer("issuer")
- .setPrincipal("userPrin")
- .setSubject("subject")
- .setIssuedAt(Instant.now())
- .setExpiresAt(Instant.now())
- .setAuthenticationTime(Instant.now())
- .setRedirectURI(new URI("http://example.com"))
- .setScope(new Scope())
- .setACR(new ACR("0"))
- .build();
- respCtx.setSubject("subject");
- respCtx.setAuthTime(Instant.now());
- respCtx.setAuthorizationGrantClaimsSet(claims);
- respCtx.setAcr("0");
- respCtx.setRedirectURI(new URI("http://example.com"));
- action = new SetAccessTokenToResponseContext();
- action.setDataSealer(getDataSealer());
- action.initialize();
- final SubjectContext subjectCtx = profileRequestCtx.getSubcontext(SubjectContext.class, true);
- subjectCtx.setPrincipalName("userPrin");
- }
-
- /**
- * Basic success case.
- *
- * @throws ComponentInitializationException
- * @throws NoSuchAlgorithmException
- * @throws URISyntaxException
- * @throws ParseException
- * @throws DataSealerException
- */
- @Test
- public void testSuccess() throws ComponentInitializationException, NoSuchAlgorithmException, URISyntaxException,
- ParseException, DataSealerException {
- init();
- final Event event = action.execute(requestCtx);
- ActionTestingSupport.assertProceedEvent(event);
- Assert.assertNotNull(respCtx.getAccessToken());
- final AccessTokenClaimsSet at =
- AccessTokenClaimsSet.parse(respCtx.getAccessToken().getValue(), getDataSealer());
- Assert.assertNotNull(at);
- }
-
- /**
- * Basic success case for non derived token.
- *
- * @throws ComponentInitializationException
- * @throws NoSuchAlgorithmException
- * @throws URISyntaxException
- * @throws ParseException
- * @throws DataSealerException
- */
- @Test
- public void testSuccess2() throws ComponentInitializationException, NoSuchAlgorithmException, URISyntaxException,
- ParseException, DataSealerException {
- init();
- respCtx.setAuthorizationGrantClaimsSet(null);
- final Event event = action.execute(requestCtx);
- ActionTestingSupport.assertProceedEvent(event);
- Assert.assertNotNull(respCtx.getAccessToken());
- final AccessTokenClaimsSet at =
- AccessTokenClaimsSet.parse(respCtx.getAccessToken().getValue(), getDataSealer());
- Assert.assertNotNull(at);
- }
-
- /**
- * Basic success case for non derived token. Test for consent.
- *
- * @throws ComponentInitializationException
- * @throws NoSuchAlgorithmException
- * @throws URISyntaxException
- * @throws ParseException
- * @throws DataSealerException
- */
- @Test
- public void testSuccess2Consent() throws ComponentInitializationException, NoSuchAlgorithmException,
- URISyntaxException, ParseException, DataSealerException {
- init();
- respCtx.setAuthorizationGrantClaimsSet(null);
- final OIDCAuthenticationResponseConsentContext consCtx =
- (OIDCAuthenticationResponseConsentContext) respCtx.addSubcontext(
- new OIDCAuthenticationResponseConsentContext());
- consCtx.getConsentedAttributes().add("3");
- final Event event = action.execute(requestCtx);
- ActionTestingSupport.assertProceedEvent(event);
- Assert.assertNotNull(respCtx.getAccessToken());
- final AccessTokenClaimsSet at =
- AccessTokenClaimsSet.parse(respCtx.getAccessToken().getValue(), getDataSealer());
- Assert.assertNotNull(at);
- Assert.assertEquals(at.getConsentedClaims(), consCtx.getConsentedAttributes());
- }
-
- /**
- * Basic success case with delivery claims.
- *
- * @throws ComponentInitializationException
- * @throws NoSuchAlgorithmException
- * @throws URISyntaxException
- * @throws ParseException
- * @throws DataSealerException
- */
- @Test
- public void testSuccessWithTokenDelivery() throws ComponentInitializationException, NoSuchAlgorithmException,
- URISyntaxException, ParseException, DataSealerException {
- init();
- final OIDCAuthenticationResponseTokenClaimsContext tokenCtx =
- (OIDCAuthenticationResponseTokenClaimsContext) respCtx.addSubcontext(
- new OIDCAuthenticationResponseTokenClaimsContext());
- tokenCtx.getClaims().setClaim("1", "1");
- tokenCtx.getIdtokenClaims().setClaim("2", "2");
- tokenCtx.getUserinfoClaims().setClaim("3", "3");
- final Event event = action.execute(requestCtx);
- ActionTestingSupport.assertProceedEvent(event);
- Assert.assertNotNull(respCtx.getAccessToken());
- final AccessTokenClaimsSet at =
- AccessTokenClaimsSet.parse(respCtx.getAccessToken().getValue(), getDataSealer());
- Assert.assertNotNull(at);
- Assert.assertNotNull(at.getDeliveryClaims().getClaim("1"));
- Assert.assertNotNull(at.getUserinfoDeliveryClaims().getClaim("3"));
- Assert.assertNull(at.getIDTokenDeliveryClaims());
- }
-
- /**
- * fails as request is of wrong type.
- *
- * @throws NoSuchAlgorithmException
- * @throws ComponentInitializationException
- * @throws URISyntaxException
- */
- @Test
- public void testFailNoAuthnReqCase2()
- throws NoSuchAlgorithmException, ComponentInitializationException, URISyntaxException {
- init();
- respCtx.setAuthorizationGrantClaimsSet(null);
- final TokenRequest req =
- new TokenRequest(new URI("http://example.com"), new RefreshTokenGrant(new RefreshToken()), null);
- setTokenRequest(req);
- final Event event = action.execute(requestCtx);
- ActionTestingSupport.assertEvent(event, EventIds.INVALID_MSG_CTX);
- }
-
- /**
- * fails as there is no subject ctx.
- *
- * @throws NoSuchAlgorithmException
- * @throws ComponentInitializationException
- * @throws URISyntaxException
- */
- @Test
- public void testFailNoSubjectCtxCase2()
- throws NoSuchAlgorithmException, ComponentInitializationException, URISyntaxException {
- init();
- respCtx.setAuthorizationGrantClaimsSet(null);
- profileRequestCtx.removeSubcontext(SubjectContext.class);
- final Event event = action.execute(requestCtx);
- ActionTestingSupport.assertEvent(event, EventIds.INVALID_PROFILE_CTX);
- }
-
- /**
- * fails as there is no rp ctx.
- *
- * @throws NoSuchAlgorithmException
- * @throws ComponentInitializationException
- * @throws URISyntaxException
- */
- @Test
- public void testFailNoRPCtx()
- throws NoSuchAlgorithmException, ComponentInitializationException, URISyntaxException {
- init();
- profileRequestCtx.removeSubcontext(RelyingPartyContext.class);
- final Event event = action.execute(requestCtx);
- ActionTestingSupport.assertEvent(event, IdPEventIds.INVALID_PROFILE_CONFIG);
- }
-
- /**
- * fails as there is no profile conf.
- *
- * @throws NoSuchAlgorithmException
- * @throws ComponentInitializationException
- * @throws URISyntaxException
- */
- @Test
- public void testFailNoProfileConf()
- throws NoSuchAlgorithmException, ComponentInitializationException, URISyntaxException {
- init();
- final RelyingPartyContext rpCtx = profileRequestCtx.getSubcontext(RelyingPartyContext.class, false);
- rpCtx.setProfileConfig(null);
- final Event event = action.execute(requestCtx);
- ActionTestingSupport.assertEvent(event, IdPEventIds.INVALID_PROFILE_CONFIG);
- }
-
- /**
- * fails as the token is of wrong type.
- *
- * @throws NoSuchAlgorithmException
- * @throws ComponentInitializationException
- * @throws URISyntaxException
- */
- @Test
- public void testFailTokenNotCodeOrRefresh()
- throws NoSuchAlgorithmException, ComponentInitializationException, URISyntaxException {
- init();
- final TokenClaimsSet claims = new AccessTokenClaimsSet.Builder()
- .setJWTID(idGenerator)
- .setClientID(new ClientID())
- .setIssuer("issuer")
- .setPrincipal("userPrin")
- .setSubject("subject")
- .setIssuedAt(Instant.now())
- .setExpiresAt(Instant.now())
- .setAuthenticationTime(Instant.now())
- .setRedirectURI(new URI("http://example.com"))
- .setScope(new Scope())
- .build();
-
- respCtx.setAuthorizationGrantClaimsSet(claims);
- final Event event = action.execute(requestCtx);
- ActionTestingSupport.assertEvent(event, EventIds.INVALID_PROFILE_CTX);
- }
-
-}
\ No newline at end of file
diff --git a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/FormOutboundUserInfoResponseMessageTest.java b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/userinfo/profile/impl/FormOutboundUserInfoResponseMessageTest.java
similarity index 96%
rename from idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/FormOutboundUserInfoResponseMessageTest.java
rename to idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/userinfo/profile/impl/FormOutboundUserInfoResponseMessageTest.java
index 192ad118..bfe4d6a6 100644
--- a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/FormOutboundUserInfoResponseMessageTest.java
+++ b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/userinfo/profile/impl/FormOutboundUserInfoResponseMessageTest.java
@@ -15,10 +15,11 @@
* limitations under the License.
*/
-package net.shibboleth.idp.plugin.oidc.op.profile.impl;
+package net.shibboleth.idp.plugin.oidc.op.userinfo.profile.impl;
import java.net.URISyntaxException;
+import net.shibboleth.idp.plugin.oidc.op.profile.impl.BaseOIDCResponseActionTest;
import net.shibboleth.idp.profile.testing.ActionTestingSupport;
import net.shibboleth.utilities.java.support.component.ComponentInitializationException;
import org.opensaml.messaging.context.MessageContext;
diff --git a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/userinfo/profile/impl/ValidateAccessTokenTest.java b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/userinfo/profile/impl/ParseAccessTokenTest.java
similarity index 58%
copy from idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/userinfo/profile/impl/ValidateAccessTokenTest.java
copy to idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/userinfo/profile/impl/ParseAccessTokenTest.java
index e2d4be55..0f6c64bf 100644
--- a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/userinfo/profile/impl/ValidateAccessTokenTest.java
+++ b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/userinfo/profile/impl/ParseAccessTokenTest.java
@@ -23,17 +23,36 @@ import net.shibboleth.idp.plugin.oidc.op.token.support.AuthorizeCodeClaimsSet;
import net.shibboleth.idp.plugin.oidc.op.token.support.TokenClaimsSet;
import net.shibboleth.idp.profile.testing.ActionTestingSupport;
import net.shibboleth.oidc.profile.core.OidcEventIds;
+import net.shibboleth.oidc.security.credential.BasicJWKCredential;
import net.shibboleth.utilities.java.support.component.ComponentInitializationException;
+import net.shibboleth.utilities.java.support.resolver.CriteriaSet;
+import net.shibboleth.utilities.java.support.resolver.ResolverException;
import net.shibboleth.utilities.java.support.security.DataSealerException;
import java.net.URI;
import java.net.URISyntaxException;
+import java.security.KeyPair;
+import java.security.KeyPairGenerator;
import java.security.NoSuchAlgorithmException;
+import java.security.interfaces.RSAPrivateKey;
+import java.security.interfaces.RSAPublicKey;
import java.time.Instant;
import java.util.Collections;
+import org.opensaml.security.credential.Credential;
+import org.opensaml.security.credential.CredentialResolver;
import org.springframework.webflow.execution.Event;
+import org.testng.annotations.BeforeClass;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;
+
+import com.nimbusds.jose.JOSEException;
+import com.nimbusds.jose.JOSEObjectType;
+import com.nimbusds.jose.JWSAlgorithm;
+import com.nimbusds.jose.JWSHeader;
+import com.nimbusds.jose.JWSSigner;
+import com.nimbusds.jose.crypto.RSASSASigner;
+import com.nimbusds.jwt.JWTClaimsSet;
+import com.nimbusds.jwt.SignedJWT;
import com.nimbusds.oauth2.sdk.Scope;
import com.nimbusds.oauth2.sdk.id.ClientID;
import com.nimbusds.oauth2.sdk.token.BearerAccessToken;
@@ -41,32 +60,51 @@ import com.nimbusds.openid.connect.sdk.UserInfoRequest;
// Checkstyle: ThrowsCount OFF
-/** {@link ValidateAccessToken} unit test. */
-public class ValidateAccessTokenTest extends BaseOIDCResponseActionTest {
+/** {@link ParseAccessToken} unit test. */
+public class ParseAccessTokenTest extends BaseOIDCResponseActionTest {
- /** Action to test. */
- private ValidateAccessToken action;
+ /** Private key for JWT signing. */
+ RSAPrivateKey rsaPrivateKey;
- @BeforeMethod
- private void init() throws ComponentInitializationException, NoSuchAlgorithmException {
- action = new ValidateAccessToken();
- action.setDataSealer(getDataSealer());
- action.setRevocationCache(new MockRevocationCache(false, true));
- action.initialize();
- }
+ /** Public key for JWT signing. */
+ RSAPublicKey rsaPublicKey;
+
+ /** Action to test. */
+ private ParseAccessToken action;
/**
- * Test that action throws error if revocation cache is not set.
+ * Init keys for JWT signing.
*
- * @throws NoSuchAlgorithmException
- * @throws ComponentInitializationException
+ * @throws NoSuchAlgorithmException on error
*/
- @Test(expectedExceptions = ComponentInitializationException.class)
- public void testNoRevocationCache() throws NoSuchAlgorithmException, ComponentInitializationException {
- action = new ValidateAccessToken();
+ @BeforeClass
+ public void initKeys() throws NoSuchAlgorithmException {
+ final KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA");
+ keyGen.initialize(2048);
+ final KeyPair keyPair = keyGen.genKeyPair();
+ rsaPrivateKey = (RSAPrivateKey) keyPair.getPrivate();
+ rsaPublicKey = (RSAPublicKey) keyPair.getPublic();
+ }
+
+ @BeforeMethod
+ private void init() throws ComponentInitializationException, NoSuchAlgorithmException {
+ action = new ParseAccessToken();
action.setDataSealer(getDataSealer());
+ action.setCredentialResolver(new CredentialResolver() {
+
+ public Iterable<Credential> resolve(CriteriaSet criteria) throws ResolverException {
+ return Collections.singletonList(resolveSingle(criteria));
+ }
+
+ public Credential resolveSingle(CriteriaSet criteria) throws ResolverException {
+ final BasicJWKCredential cred = new BasicJWKCredential();
+ cred.setPublicKey(rsaPublicKey);
+ cred.setPrivateKey(rsaPrivateKey);
+ return cred;
+ }
+
+ });
action.initialize();
- action.execute(requestCtx);
}
/**
@@ -99,99 +137,77 @@ public class ValidateAccessTokenTest extends BaseOIDCResponseActionTest {
ActionTestingSupport.assertProceedEvent(event);
}
+
/**
- * Fails due to access token containing an audience (ours never do).
+ * Basic success case with signed JWT.
*
* @throws NoSuchAlgorithmException
* @throws ComponentInitializationException
* @throws URISyntaxException
* @throws DataSealerException
+ * @throws JOSEException
*/
@Test
- public void testFailsAudience()
- throws NoSuchAlgorithmException, ComponentInitializationException, URISyntaxException, DataSealerException {
+ public void testJWT()
+ throws NoSuchAlgorithmException, ComponentInitializationException, URISyntaxException, DataSealerException, JOSEException {
+
+ final String sealedClaims = getDataSealer().wrap(
+ new JWTClaimsSet.Builder().claim(TokenClaimsSet.KEY_USER_PRINCIPAL, "userPrin").build().toString());
+
final TokenClaimsSet claims = new AccessTokenClaimsSet.Builder()
.setJWTID(idGenerator)
.setClientID(new ClientID())
.setIssuer("issuer")
- .setPrincipal("userPrin")
.setSubject("subject")
.setIssuedAt(Instant.now())
.setExpiresAt(Instant.now().plusSeconds(1))
.setAuthenticationTime(Instant.now())
.setRedirectURI(new URI("http://example.com"))
.setScope(new Scope())
- .setAudience(Collections.singletonList("foo"))
+ .addCustomClaim(TokenClaimsSet.KEY_SEALED_FOR_OP, sealedClaims)
.build();
- final BearerAccessToken token = new BearerAccessToken(claims.serialize(getDataSealer()));
- final UserInfoRequest req = new UserInfoRequest(new URI("http://example.com"), token);
+
+ JWSSigner signer = new RSASSASigner(rsaPrivateKey);
+ JWSHeader.Builder headerBuilder = new JWSHeader.Builder(new JWSAlgorithm("RS256")).type(new JOSEObjectType("at+jwt"));
+ SignedJWT jwt = new SignedJWT(headerBuilder.build(), claims.getClaimsSet());
+ jwt.sign(signer);
+
+ BearerAccessToken token = new BearerAccessToken(jwt.serialize());
+ UserInfoRequest req = new UserInfoRequest(new URI("http://example.com"), token);
setUserInfoRequest(req);
- final Event event = action.execute(requestCtx);
- ActionTestingSupport.assertEvent(event, OidcEventIds.INVALID_GRANT);
- }
-
- /**
- * Fails due to access token being substituted with authorize code.
- *
- * @throws NoSuchAlgorithmException
- * @throws ComponentInitializationException
- * @throws URISyntaxException
- * @throws DataSealerException
- */
- @Test
- public void testFailsNotAccessToken()
- throws NoSuchAlgorithmException, ComponentInitializationException, URISyntaxException, DataSealerException {
- final TokenClaimsSet claims = new AuthorizeCodeClaimsSet.Builder()
- .setJWTID(idGenerator)
- .setClientID(new ClientID())
- .setIssuer("issuer")
- .setPrincipal("userPrin")
- .setSubject("subject")
- .setIssuedAt(Instant.now())
- .setExpiresAt(Instant.now().plusSeconds(1))
- .setAuthenticationTime(Instant.now())
- .setRedirectURI(new URI("http://example.com"))
- .setScope(new Scope())
- .build();
- final BearerAccessToken token = new BearerAccessToken(claims.serialize(getDataSealer()));
- final UserInfoRequest req = new UserInfoRequest(new URI("http://example.com"), token);
+
+ Event event = action.execute(requestCtx);
+ ActionTestingSupport.assertProceedEvent(event);
+
+ // Wrong token type.
+ headerBuilder = new JWSHeader.Builder(new JWSAlgorithm("RS256"));
+ jwt = new SignedJWT(headerBuilder.build(), claims.getClaimsSet());
+ jwt.sign(signer);
+ token = new BearerAccessToken(jwt.serialize());
+ req = new UserInfoRequest(new URI("http://example.com"), token);
setUserInfoRequest(req);
- final Event event = action.execute(requestCtx);
+
+ event = action.execute(requestCtx);
ActionTestingSupport.assertEvent(event, OidcEventIds.INVALID_GRANT);
- }
- /**
- * Fails due token expiration.
- *
- * @throws NoSuchAlgorithmException
- * @throws ComponentInitializationException
- * @throws URISyntaxException
- * @throws DataSealerException
- */
- @Test
- public void testFailsExpired()
- throws NoSuchAlgorithmException, ComponentInitializationException, URISyntaxException, DataSealerException {
- final TokenClaimsSet claims = new AccessTokenClaimsSet.Builder()
- .setJWTID(idGenerator)
- .setClientID(new ClientID())
- .setIssuer("issuer")
- .setPrincipal("userPrin")
- .setSubject("subject")
- .setIssuedAt(Instant.now())
- .setExpiresAt(Instant.now().minusMillis(1))
- .setAuthenticationTime(Instant.now())
- .setRedirectURI(new URI("http://example.com"))
- .setScope(new Scope())
- .build();
- final BearerAccessToken token = new BearerAccessToken(claims.serialize(getDataSealer()));
- final UserInfoRequest req = new UserInfoRequest(new URI("http://example.com"), token);
+ // Sign with wrong key.
+ final KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA");
+ keyGen.initialize(2048);
+ final KeyPair keyPair = keyGen.genKeyPair();
+ signer = new RSASSASigner(keyPair.getPrivate());
+ headerBuilder = new JWSHeader.Builder(new JWSAlgorithm("RS256")).type(new JOSEObjectType("at+jwt"));
+ jwt = new SignedJWT(headerBuilder.build(), claims.getClaimsSet());
+ jwt.sign(signer);
+ token = new BearerAccessToken(jwt.serialize());
+ req = new UserInfoRequest(new URI("http://example.com"), token);
setUserInfoRequest(req);
- final Event event = action.execute(requestCtx);
+
+ event = action.execute(requestCtx);
ActionTestingSupport.assertEvent(event, OidcEventIds.INVALID_GRANT);
}
/**
- * Fails due token authz code is revoked. Test not 100% as it really does not test passing id to revocation cache.
+ * Fails due to access token being substituted with authorize code.
*
* @throws NoSuchAlgorithmException
* @throws ComponentInitializationException
@@ -199,13 +215,9 @@ public class ValidateAccessTokenTest extends BaseOIDCResponseActionTest {
* @throws DataSealerException
*/
@Test
- public void testFailsRevoked()
+ public void testFailsNotAccessToken()
throws NoSuchAlgorithmException, ComponentInitializationException, URISyntaxException, DataSealerException {
- action = new ValidateAccessToken();
- action.setDataSealer(getDataSealer());
- action.setRevocationCache(new MockRevocationCache(true, true));
- action.initialize();
- final TokenClaimsSet claims = new AccessTokenClaimsSet.Builder()
+ final TokenClaimsSet claims = new AuthorizeCodeClaimsSet.Builder()
.setJWTID(idGenerator)
.setClientID(new ClientID())
.setIssuer("issuer")
diff --git a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/SignUserInfoResponseTest.java b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/userinfo/profile/impl/SignUserInfoResponseTest.java
similarity index 98%
rename from idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/SignUserInfoResponseTest.java
rename to idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/userinfo/profile/impl/SignUserInfoResponseTest.java
index c0be0f03..27e57542 100644
--- a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/SignUserInfoResponseTest.java
+++ b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/userinfo/profile/impl/SignUserInfoResponseTest.java
@@ -15,12 +15,13 @@
* limitations under the License.
*/
-package net.shibboleth.idp.plugin.oidc.op.profile.impl;
+package net.shibboleth.idp.plugin.oidc.op.userinfo.profile.impl;
import java.net.URISyntaxException;
import java.security.interfaces.ECPublicKey;
import java.security.interfaces.RSAPublicKey;
+import net.shibboleth.idp.plugin.oidc.op.profile.impl.BaseOIDCResponseActionTest;
import net.shibboleth.idp.profile.testing.ActionTestingSupport;
import net.shibboleth.utilities.java.support.component.ComponentInitializationException;
diff --git a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/userinfo/profile/impl/ValidateAccessTokenTest.java b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/userinfo/profile/impl/ValidateAccessTokenTest.java
index e2d4be55..c613e05f 100644
--- a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/userinfo/profile/impl/ValidateAccessTokenTest.java
+++ b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/userinfo/profile/impl/ValidateAccessTokenTest.java
@@ -19,25 +19,32 @@ package net.shibboleth.idp.plugin.oidc.op.userinfo.profile.impl;
import net.shibboleth.idp.plugin.oidc.op.profile.impl.BaseOIDCResponseActionTest;
import net.shibboleth.idp.plugin.oidc.op.token.support.AccessTokenClaimsSet;
-import net.shibboleth.idp.plugin.oidc.op.token.support.AuthorizeCodeClaimsSet;
import net.shibboleth.idp.plugin.oidc.op.token.support.TokenClaimsSet;
import net.shibboleth.idp.profile.testing.ActionTestingSupport;
+import net.shibboleth.oidc.jwt.claims.ClaimsValidator;
import net.shibboleth.oidc.profile.core.OidcEventIds;
+import net.shibboleth.oidc.security.jwt.claims.impl.AudienceClaimsValidator;
+import net.shibboleth.oidc.security.jwt.claims.impl.ChainingJWTClaimsValidator;
+import net.shibboleth.oidc.security.jwt.claims.impl.ExpiryClaimsValidator;
+import net.shibboleth.oidc.security.jwt.claims.impl.NotBeforeClaimsValidator;
+import net.shibboleth.oidc.security.jwt.claims.impl.RequiredClaimsValidator;
import net.shibboleth.utilities.java.support.component.ComponentInitializationException;
+import net.shibboleth.utilities.java.support.logic.BiFunctionSupport;
import net.shibboleth.utilities.java.support.security.DataSealerException;
import java.net.URI;
import java.net.URISyntaxException;
import java.security.NoSuchAlgorithmException;
import java.time.Instant;
+import java.util.ArrayList;
import java.util.Collections;
+import java.util.function.Function;
+import org.opensaml.profile.context.ProfileRequestContext;
import org.springframework.webflow.execution.Event;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;
import com.nimbusds.oauth2.sdk.Scope;
import com.nimbusds.oauth2.sdk.id.ClientID;
-import com.nimbusds.oauth2.sdk.token.BearerAccessToken;
-import com.nimbusds.openid.connect.sdk.UserInfoRequest;
// Checkstyle: ThrowsCount OFF
@@ -50,25 +57,10 @@ public class ValidateAccessTokenTest extends BaseOIDCResponseActionTest {
@BeforeMethod
private void init() throws ComponentInitializationException, NoSuchAlgorithmException {
action = new ValidateAccessToken();
- action.setDataSealer(getDataSealer());
- action.setRevocationCache(new MockRevocationCache(false, true));
+ action.setClaimsValidatorLookupStrategy(new ClaimsValidatorLookup());
action.initialize();
}
- /**
- * Test that action throws error if revocation cache is not set.
- *
- * @throws NoSuchAlgorithmException
- * @throws ComponentInitializationException
- */
- @Test(expectedExceptions = ComponentInitializationException.class)
- public void testNoRevocationCache() throws NoSuchAlgorithmException, ComponentInitializationException {
- action = new ValidateAccessToken();
- action.setDataSealer(getDataSealer());
- action.initialize();
- action.execute(requestCtx);
- }
-
/**
* Basic success case.
*
@@ -87,14 +79,13 @@ public class ValidateAccessTokenTest extends BaseOIDCResponseActionTest {
.setPrincipal("userPrin")
.setSubject("subject")
.setIssuedAt(Instant.now())
- .setExpiresAt(Instant.now().plusSeconds(1))
+ .setExpiresAt(Instant.now().plusSeconds(300))
.setAuthenticationTime(Instant.now())
.setRedirectURI(new URI("http://example.com"))
.setScope(new Scope())
.build();
- final BearerAccessToken token = new BearerAccessToken(claims.serialize(getDataSealer()));
- final UserInfoRequest req = new UserInfoRequest(new URI("http://example.com"), token);
- setUserInfoRequest(req);
+ respCtx.setAuthorizationGrantClaimsSet(claims);
+
final Event event = action.execute(requestCtx);
ActionTestingSupport.assertProceedEvent(event);
}
@@ -117,45 +108,14 @@ public class ValidateAccessTokenTest extends BaseOIDCResponseActionTest {
.setPrincipal("userPrin")
.setSubject("subject")
.setIssuedAt(Instant.now())
- .setExpiresAt(Instant.now().plusSeconds(1))
+ .setExpiresAt(Instant.now().plusSeconds(300))
.setAuthenticationTime(Instant.now())
.setRedirectURI(new URI("http://example.com"))
.setScope(new Scope())
.setAudience(Collections.singletonList("foo"))
.build();
- final BearerAccessToken token = new BearerAccessToken(claims.serialize(getDataSealer()));
- final UserInfoRequest req = new UserInfoRequest(new URI("http://example.com"), token);
- setUserInfoRequest(req);
- final Event event = action.execute(requestCtx);
- ActionTestingSupport.assertEvent(event, OidcEventIds.INVALID_GRANT);
- }
-
- /**
- * Fails due to access token being substituted with authorize code.
- *
- * @throws NoSuchAlgorithmException
- * @throws ComponentInitializationException
- * @throws URISyntaxException
- * @throws DataSealerException
- */
- @Test
- public void testFailsNotAccessToken()
- throws NoSuchAlgorithmException, ComponentInitializationException, URISyntaxException, DataSealerException {
- final TokenClaimsSet claims = new AuthorizeCodeClaimsSet.Builder()
- .setJWTID(idGenerator)
- .setClientID(new ClientID())
- .setIssuer("issuer")
- .setPrincipal("userPrin")
- .setSubject("subject")
- .setIssuedAt(Instant.now())
- .setExpiresAt(Instant.now().plusSeconds(1))
- .setAuthenticationTime(Instant.now())
- .setRedirectURI(new URI("http://example.com"))
- .setScope(new Scope())
- .build();
- final BearerAccessToken token = new BearerAccessToken(claims.serialize(getDataSealer()));
- final UserInfoRequest req = new UserInfoRequest(new URI("http://example.com"), token);
- setUserInfoRequest(req);
+ respCtx.setAuthorizationGrantClaimsSet(claims);
+
final Event event = action.execute(requestCtx);
ActionTestingSupport.assertEvent(event, OidcEventIds.INVALID_GRANT);
}
@@ -178,50 +138,40 @@ public class ValidateAccessTokenTest extends BaseOIDCResponseActionTest {
.setPrincipal("userPrin")
.setSubject("subject")
.setIssuedAt(Instant.now())
- .setExpiresAt(Instant.now().minusMillis(1))
+ .setExpiresAt(Instant.now().minusSeconds(120))
.setAuthenticationTime(Instant.now())
.setRedirectURI(new URI("http://example.com"))
.setScope(new Scope())
.build();
- final BearerAccessToken token = new BearerAccessToken(claims.serialize(getDataSealer()));
- final UserInfoRequest req = new UserInfoRequest(new URI("http://example.com"), token);
- setUserInfoRequest(req);
+ respCtx.setAuthorizationGrantClaimsSet(claims);
+
final Event event = action.execute(requestCtx);
ActionTestingSupport.assertEvent(event, OidcEventIds.INVALID_GRANT);
}
- /**
- * Fails due token authz code is revoked. Test not 100% as it really does not test passing id to revocation cache.
- *
- * @throws NoSuchAlgorithmException
- * @throws ComponentInitializationException
- * @throws URISyntaxException
- * @throws DataSealerException
- */
- @Test
- public void testFailsRevoked()
- throws NoSuchAlgorithmException, ComponentInitializationException, URISyntaxException, DataSealerException {
- action = new ValidateAccessToken();
- action.setDataSealer(getDataSealer());
- action.setRevocationCache(new MockRevocationCache(true, true));
- action.initialize();
- final TokenClaimsSet claims = new AccessTokenClaimsSet.Builder()
- .setJWTID(idGenerator)
- .setClientID(new ClientID())
- .setIssuer("issuer")
- .setPrincipal("userPrin")
- .setSubject("subject")
- .setIssuedAt(Instant.now())
- .setExpiresAt(Instant.now().plusSeconds(1))
- .setAuthenticationTime(Instant.now())
- .setRedirectURI(new URI("http://example.com"))
- .setScope(new Scope())
- .build();
- final BearerAccessToken token = new BearerAccessToken(claims.serialize(getDataSealer()));
- final UserInfoRequest req = new UserInfoRequest(new URI("http://example.com"), token);
- setUserInfoRequest(req);
- final Event event = action.execute(requestCtx);
- ActionTestingSupport.assertEvent(event, OidcEventIds.INVALID_GRANT);
- }
+ private class ClaimsValidatorLookup implements Function<ProfileRequestContext,ClaimsValidator> {
+ public ClaimsValidator apply(ProfileRequestContext t) {
+ final ChainingJWTClaimsValidator chain = new ChainingJWTClaimsValidator();
+ chain.setId("test");
+ chain.setRequireAll(true);
+
+ final ArrayList<ClaimsValidator> validators = new ArrayList<>();
+ final RequiredClaimsValidator req = new RequiredClaimsValidator();
+ req.setRequiredClaims(Collections.singletonList("jti"));
+ validators.add(req);
+ validators.add(new NotBeforeClaimsValidator());
+ validators.add(new ExpiryClaimsValidator());
+ final AudienceClaimsValidator aud = new AudienceClaimsValidator();
+ aud.setAudienceLookupStrategy(BiFunctionSupport.constant("issuer"));
+ aud.setAllowMissing(true);
+ validators.add(aud);
+
+ chain.setClaimValidators(validators);
+
+ return chain;
+ }
+
+ }
+
}
\ No newline at end of file
diff --git a/pom.xml b/pom.xml
index a163b99d..213f68ac 100644
--- a/pom.xml
+++ b/pom.xml
@@ -9,13 +9,13 @@
</parent>
<groupId>net.shibboleth.idp.plugin.oidc</groupId>
<artifactId>idp-plugin-oidc-op-parent</artifactId>
- <version>3.1.2-SNAPSHOT</version>
+ <version>3.2.0-SNAPSHOT</version>
<name>Shibboleth IdP :: Plugins :: OpenID Connect Provider</name>
<packaging>pom</packaging>
<properties>
<shib.idp.version>4.2.0</shib.idp.version>
<opensaml.version>4.2.0</opensaml.version>
- <oidc.common.version>2.0.0</oidc.common.version>
+ <oidc.common.version>2.0.1-SNAPSHOT</oidc.common.version>
<gson.version>2.8.6</gson.version>
<commons.io.version>2.6</commons.io.version>
<checkstyle.configLocation>${project.basedir}/resources/checkstyle.xml</checkstyle.configLocation>
--
To stop receiving notification emails like this one, please contact
the administrator of this repository.
More information about the commits
mailing list