[java-plugin-shibd-oidc] branch main updated: WIP: Complete the token consumer flow past the UserInfo lookup
Codeberg
noreply at shibboleth.net
Wed Nov 19 16:41:13 UTC 2025
This is an automated email from the git hooks/post-receive script.
codeberg pushed a commit to branch main
in repository java-plugin-shibd-oidc.
View the commit online:
https://codeberg.org/Shibboleth/java-plugin-shibd-oidc/commit/0a834fc98ed618e78b578332ce4efe7d68ff31d0
The following commit(s) were added to refs/heads/main by this push:
new 0a834fc WIP: Complete the token consumer flow past the UserInfo lookup
0a834fc is described below
commit 0a834fc98ed618e78b578332ce4efe7d68ff31d0
Author: Phil Smart <philip.smart at jisc.ac.uk>
AuthorDate: Wed Nov 19 16:41:02 2025 +0000
WIP: Complete the token consumer flow past the UserInfo lookup
- UserInfo lookup is now operational
- Some UserInfo response validation is in place (inc. signature
checks). But some have been disabled as they need to be fixed for the
id_token first.
- Add a dummy OP signing key and Mock up an OP JWKS response that
includes the public component of the key
- Improve the flow test logic
---
...bstractUserInfoTokenResponseLookupStrategy.java | 66 ++++
.../EncryptedUserInfoJWTLookupStrategy.java | 91 ++++++
...nfoInUserInfoResponseContextUpdateStrategy.java | 75 +++++
.../navigate/UserInfoJWTLookupStrategy.java | 76 +++++
.../sp/oidc/exception/OIDCRPException.java | 65 ----
.../AbstractUserInfoResponseTypeCondition.java | 84 +++++
.../logic/UserInfoPlainResponseTypeCondition.java | 59 ++++
.../META-INF/net.shibboleth.idp/postconfig.xml | 23 --
.../idp/flows/sp/consumer/oidc/oidc-beans.xml | 155 ++++++++-
.../idp/flows/sp/consumer/oidc/oidc-flow.xml | 28 +-
.../shibboleth/idp/flows/sp/oidc-common-beans.xml | 9 +
.../net/shibboleth/sp/service/agent/postconfig.xml | 29 +-
.../sp/oidc/flows/OIDCTokenConsumerFlowTest.java | 247 +++++++++------
.../shibboleth/sp/oidc/flows/TestConstants.java | 74 +++++
.../shibboleth/sp/oidc/flows/TestTokenHelper.java | 349 +++++++++++++++++++++
.../resources/metadata/openid-configuration.json | 8 +-
.../idp/module/credentials/op/op-signing-rsa.jwk | 14 +
.../net/shibboleth/sp/oidc-test-beans.xml | 16 +-
.../impl/AbstractAuthorizationResponseAction.java | 4 +-
.../oidc/profile/impl/AbstractHttpOAuthAction.java | 14 +-
.../profile/impl/DefaultClaimMergingStrategy.java | 83 +++++
.../impl/DefaultClaimSanitizationStrategy.java | 72 +++++
.../profile/impl/ExchangeCodeForAccessToken.java | 6 +-
.../sp/oidc/profile/impl/ProcessEndUserClaims.java | 293 +++++++++++++++++
.../oidc/profile/impl/UserInfoEndpointLookup.java | 107 +++++++
.../sp/oidc/profile/impl/ValidateTokenClaims.java | 8 +-
.../impl/ValidateUserInfoJSONObjectClaims.java | 221 +++++++++++++
27 files changed, 2017 insertions(+), 259 deletions(-)
diff --git a/sp-oidc-api/src/main/java/net/shibboleth/sp/oidc/context/navigate/AbstractUserInfoTokenResponseLookupStrategy.java b/sp-oidc-api/src/main/java/net/shibboleth/sp/oidc/context/navigate/AbstractUserInfoTokenResponseLookupStrategy.java
new file mode 100644
index 0000000..6492be9
--- /dev/null
+++ b/sp-oidc-api/src/main/java/net/shibboleth/sp/oidc/context/navigate/AbstractUserInfoTokenResponseLookupStrategy.java
@@ -0,0 +1,66 @@
+/*
+ * Licensed 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.sp.oidc.context.navigate;
+
+import java.util.function.Function;
+
+import javax.annotation.Nonnull;
+
+import org.opensaml.messaging.context.navigate.ChildContextLookup;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.opensaml.profile.context.navigate.InboundMessageContextLookup;
+
+import net.shibboleth.oidc.profile.context.UserInfoResponseContext;
+import net.shibboleth.shared.logic.Constraint;
+
+/** Base class for looking up the UserInfo token response context.*/
+public abstract class AbstractUserInfoTokenResponseLookupStrategy {
+
+ /** Strategy used to look up the {@link UserInfoResponseContext}. */
+ @Nonnull private final Function<ProfileRequestContext, UserInfoResponseContext>
+ userInfoResponseContextLookupStrategy;
+
+
+ /** Constructor.*/
+ protected AbstractUserInfoTokenResponseLookupStrategy() {
+ userInfoResponseContextLookupStrategy =
+ new ChildContextLookup<>(UserInfoResponseContext.class, true).compose(
+ new InboundMessageContextLookup());
+ }
+
+ /**
+ *
+ * Constructor.
+ *
+ * @param strategy the strategy used look up the {@link UserInfoResponseContext}.
+ */
+ protected AbstractUserInfoTokenResponseLookupStrategy(final
+ Function<ProfileRequestContext, UserInfoResponseContext> strategy) {
+
+ userInfoResponseContextLookupStrategy =
+ Constraint.isNotNull(strategy, "userInfoResponseContextLookupStrategy can not be null");
+ }
+
+ /**
+ * Get the UserInfo response context lookup strategy.
+ *
+ * @return the lookup strategy
+ */
+ @Nonnull
+ protected Function<ProfileRequestContext, UserInfoResponseContext> getUserInfoResponseContextLookupStrategy() {
+ return userInfoResponseContextLookupStrategy;
+ }
+
+}
diff --git a/sp-oidc-api/src/main/java/net/shibboleth/sp/oidc/context/navigate/EncryptedUserInfoJWTLookupStrategy.java b/sp-oidc-api/src/main/java/net/shibboleth/sp/oidc/context/navigate/EncryptedUserInfoJWTLookupStrategy.java
new file mode 100644
index 0000000..7ea0dc4
--- /dev/null
+++ b/sp-oidc-api/src/main/java/net/shibboleth/sp/oidc/context/navigate/EncryptedUserInfoJWTLookupStrategy.java
@@ -0,0 +1,91 @@
+/*
+ * Licensed 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.sp.oidc.context.navigate;
+
+import java.util.function.Function;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.ThreadSafe;
+
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.slf4j.Logger;
+
+import com.nimbusds.jwt.EncryptedJWT;
+import com.nimbusds.jwt.JWT;
+import com.nimbusds.jwt.SignedJWT;
+import com.nimbusds.openid.connect.sdk.UserInfoSuccessResponse;
+
+import net.shibboleth.oidc.profile.context.AccessTokenResponseContext;
+import net.shibboleth.oidc.profile.context.UserInfoResponseContext;
+import net.shibboleth.shared.annotation.ParameterName;
+import net.shibboleth.shared.primitive.LoggerFactory;
+
+/**
+ * Function that extracts the UserInfo JWT from the {@link UserInfoResponseContext} iff it is an {@link EncryptedJWT}
+ * type. If not {@code null} is returned.
+ */
+ at ThreadSafe
+public class EncryptedUserInfoJWTLookupStrategy extends AbstractUserInfoTokenResponseLookupStrategy
+ implements Function<ProfileRequestContext, JWT>{
+
+ /** Logger. */
+ @Nonnull private final Logger log = LoggerFactory.getLogger(EncryptedUserInfoJWTLookupStrategy.class);
+
+ /**
+ *
+ * Constructor.
+ *
+ * @param strategy the strategy used look up the {@link AccessTokenResponseContext}.
+ */
+ public EncryptedUserInfoJWTLookupStrategy(
+ @ParameterName(name="userInfoResponseContextLookupStrategy") final
+ Function<ProfileRequestContext, UserInfoResponseContext> strategy) {
+ super(strategy);
+ }
+
+ /** Constructor.*/
+ public EncryptedUserInfoJWTLookupStrategy() {
+ super();
+ }
+
+ @Override
+ @Nullable public JWT apply(@Nullable final ProfileRequestContext prc) {
+ final UserInfoResponseContext userInfoContext = getUserInfoResponseContextLookupStrategy().apply(prc);
+ final UserInfoSuccessResponse userInfo = userInfoContext != null ? userInfoContext.getUserInfo() : null;
+ if (userInfo == null) {
+ return null;
+ }
+ if (userInfo.getUserInfoJWT() == null) {
+ log.trace("EncryptedUserInfoToken Lookup: UserInfo response JWT is null, nothing to return");
+ return null;
+ }
+ final JWT token = userInfo.getUserInfoJWT();
+ if (token instanceof EncryptedJWT) {
+ log.trace("EncryptedUserInfoToken Lookup: UserInfo response JWT is encrypted using algorithm '{}'",
+ token.getHeader().getAlgorithm());
+ return token;
+ } else if (token instanceof SignedJWT){
+ log.trace("EncryptedUserInfoToken Lookup: UserInfo response JWT is signed and not encrypted,"
+ + " nothing to return");
+ return null;
+ } else {
+ log.trace("EncryptedUserInfoToken Lookup: UserInfo response JWT is neither signed nor encrypted, "
+ + "nothing to return");
+ return null;
+ }
+ }
+
+}
diff --git a/sp-oidc-api/src/main/java/net/shibboleth/sp/oidc/context/navigate/UserInfoInUserInfoResponseContextUpdateStrategy.java b/sp-oidc-api/src/main/java/net/shibboleth/sp/oidc/context/navigate/UserInfoInUserInfoResponseContextUpdateStrategy.java
new file mode 100644
index 0000000..0110089
--- /dev/null
+++ b/sp-oidc-api/src/main/java/net/shibboleth/sp/oidc/context/navigate/UserInfoInUserInfoResponseContextUpdateStrategy.java
@@ -0,0 +1,75 @@
+/*
+ * Licensed 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.sp.oidc.context.navigate;
+
+import java.util.function.BiConsumer;
+import java.util.function.Function;
+
+import javax.annotation.Nonnull;
+
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.slf4j.Logger;
+
+import com.nimbusds.jwt.JWT;
+import com.nimbusds.openid.connect.sdk.UserInfoResponse;
+import com.nimbusds.openid.connect.sdk.UserInfoSuccessResponse;
+
+import net.shibboleth.oidc.profile.context.AccessTokenResponseContext;
+import net.shibboleth.oidc.profile.context.UserInfoResponseContext;
+import net.shibboleth.shared.annotation.ParameterName;
+import net.shibboleth.shared.primitive.LoggerFactory;
+
+/**
+ * Consumer strategy to update the UserInfo JWT in the {@link UserInfoResponseContext}.
+ * Note, replaces the entire {@link UserInfoResponse} object.
+ */
+public class UserInfoInUserInfoResponseContextUpdateStrategy extends AbstractUserInfoTokenResponseLookupStrategy
+ implements BiConsumer<ProfileRequestContext, JWT> {
+
+ /** Class logger. */
+ @Nonnull private final Logger log = LoggerFactory.getLogger(UserInfoInUserInfoResponseContextUpdateStrategy.class);
+
+ /**
+ *
+ * Constructor.
+ *
+ * @param strategy the strategy used look up the {@link AccessTokenResponseContext}.
+ */
+ public UserInfoInUserInfoResponseContextUpdateStrategy(
+ @ParameterName(name="userInfoResponseContextLookupStrategy") final
+ Function<ProfileRequestContext, UserInfoResponseContext> strategy) {
+ super(strategy);
+ }
+
+ /** Constructor.*/
+ public UserInfoInUserInfoResponseContextUpdateStrategy() {
+ super();
+ }
+
+ @Override
+ public void accept(final ProfileRequestContext profileRequestContext, final JWT token) {
+
+ final UserInfoResponseContext context =
+ getUserInfoResponseContextLookupStrategy().apply(profileRequestContext);
+ if (context != null) {
+ // Create a new UserInfo element with the new JWT response
+ context.setUserInfo(new UserInfoSuccessResponse(token));
+ } else {
+ log.warn("Unable to set UserInfo back onto response context");
+ }
+
+ }
+
+}
diff --git a/sp-oidc-api/src/main/java/net/shibboleth/sp/oidc/context/navigate/UserInfoJWTLookupStrategy.java b/sp-oidc-api/src/main/java/net/shibboleth/sp/oidc/context/navigate/UserInfoJWTLookupStrategy.java
new file mode 100644
index 0000000..951857a
--- /dev/null
+++ b/sp-oidc-api/src/main/java/net/shibboleth/sp/oidc/context/navigate/UserInfoJWTLookupStrategy.java
@@ -0,0 +1,76 @@
+/*
+ * Licensed 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.sp.oidc.context.navigate;
+
+import java.util.function.Function;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.ThreadSafe;
+
+import org.opensaml.messaging.context.navigate.ChildContextLookup;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.opensaml.profile.context.navigate.InboundMessageContextLookup;
+
+import com.nimbusds.common.contenttype.ContentType;
+import com.nimbusds.jwt.JWT;
+import com.nimbusds.openid.connect.sdk.UserInfoSuccessResponse;
+
+import net.shibboleth.oidc.profile.context.UserInfoResponseContext;
+import net.shibboleth.shared.logic.Constraint;
+
+/**
+ * Function that extracts the UserInfo claims as a JWT from the {@link UserInfoResponseContext}. If not found, returns
+ * {@code null}.
+ */
+ at ThreadSafe
+public class UserInfoJWTLookupStrategy implements Function<ProfileRequestContext, JWT> {
+
+ /** Strategy used to look up the {@link UserInfoResponseContext}. */
+ @Nonnull private final Function<ProfileRequestContext, UserInfoResponseContext>
+ userInfoResponseContextLookupStrategy;
+
+ /** Constructor.*/
+ public UserInfoJWTLookupStrategy() {
+ userInfoResponseContextLookupStrategy =
+ new ChildContextLookup<>(UserInfoResponseContext.class).compose(
+ new InboundMessageContextLookup());
+ }
+
+ /**
+ *
+ * Constructor.
+ *
+ * @param strategy the UserInfo response context lookup strategy to use.
+ */
+ public UserInfoJWTLookupStrategy(
+ @Nonnull final Function<ProfileRequestContext, UserInfoResponseContext> strategy) {
+ userInfoResponseContextLookupStrategy =
+ Constraint.isNotNull(strategy, "UserInfoResponseContext lookup strategy can not be null");
+ }
+
+
+ @Override
+ @Nullable public JWT apply(@Nullable final ProfileRequestContext prc) {
+
+ final UserInfoResponseContext userInfoContext = userInfoResponseContextLookupStrategy.apply(prc);
+ final UserInfoSuccessResponse userInfo = userInfoContext != null ? userInfoContext.getUserInfo() : null;
+ if (userInfo == null || userInfo.getEntityContentType() != ContentType.APPLICATION_JWT) {
+ return null;
+ }
+ return userInfo.getUserInfoJWT();
+ }
+
+}
diff --git a/sp-oidc-api/src/main/java/net/shibboleth/sp/oidc/exception/OIDCRPException.java b/sp-oidc-api/src/main/java/net/shibboleth/sp/oidc/exception/OIDCRPException.java
deleted file mode 100644
index e928416..0000000
--- a/sp-oidc-api/src/main/java/net/shibboleth/sp/oidc/exception/OIDCRPException.java
+++ /dev/null
@@ -1,65 +0,0 @@
-/*
- * Licensed 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.sp.oidc.exception;
-
-import javax.annotation.concurrent.ThreadSafe;
-
-/**
- * An exception to signal a general OIDC RelyingParty error.
- */
- at ThreadSafe
-public class OIDCRPException extends Exception{
-
- /** Serial UID. */
- private static final long serialVersionUID = -2380145079984333546L;
-
- /** Constructor. */
- public OIDCRPException() {
- super();
-
- }
-
- /**
- * Constructor.
- *
- * @param message exception message
- * @param cause exception to be wrapped by this one
- */
- public OIDCRPException(final String message, final Throwable cause) {
- super(message, cause);
-
- }
-
- /**
- * Constructor.
- *
- * @param message exception message
- */
- public OIDCRPException(final String message) {
- super(message);
-
- }
-
- /**
- * Constructor.
- *
- * @param cause exception to be wrapped by this one
- */
- public OIDCRPException(final Throwable cause) {
- super(cause);
-
- }
-
-}
diff --git a/sp-oidc-api/src/main/java/net/shibboleth/sp/oidc/messaging/context/logic/AbstractUserInfoResponseTypeCondition.java b/sp-oidc-api/src/main/java/net/shibboleth/sp/oidc/messaging/context/logic/AbstractUserInfoResponseTypeCondition.java
new file mode 100644
index 0000000..6d865c6
--- /dev/null
+++ b/sp-oidc-api/src/main/java/net/shibboleth/sp/oidc/messaging/context/logic/AbstractUserInfoResponseTypeCondition.java
@@ -0,0 +1,84 @@
+/*
+ * Licensed 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.sp.oidc.messaging.context.logic;
+
+import java.util.function.Function;
+import java.util.function.Predicate;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.messaging.context.navigate.ChildContextLookup;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.opensaml.profile.context.navigate.InboundMessageContextLookup;
+
+import net.shibboleth.oidc.profile.context.UserInfoResponseContext;
+import net.shibboleth.shared.logic.Constraint;
+
+/**
+ * Abstract predicate for pulling out the {@link UserInfoResponseContext}. If either the
+ * profile request context or extracted UserInfo response context are {@code null} false is returned
+ * immediately.
+ */
+public abstract class AbstractUserInfoResponseTypeCondition implements Predicate<ProfileRequestContext> {
+
+ /** Strategy used to look up the {@link UserInfoResponseContext}. */
+ @Nonnull private final Function<ProfileRequestContext, UserInfoResponseContext>
+ userInfoResponseContextLookupStrategy;
+
+ /** Constructor.*/
+ protected AbstractUserInfoResponseTypeCondition() {
+ userInfoResponseContextLookupStrategy =
+ new ChildContextLookup<>(UserInfoResponseContext.class).compose(
+ new InboundMessageContextLookup());
+ }
+
+ /**
+ *
+ * Constructor.
+ *
+ * @param strategy the UserInfo response context lookup strategy to use.
+ */
+ protected AbstractUserInfoResponseTypeCondition(
+ @Nonnull final Function<ProfileRequestContext, UserInfoResponseContext> strategy) {
+ userInfoResponseContextLookupStrategy =
+ Constraint.isNotNull(strategy, "UserInfoResponseContext lookup strategy can not be null");
+ }
+
+ @Override
+ public boolean test(@Nullable final ProfileRequestContext prc) {
+ if (prc == null) {
+ return false;
+ }
+ final UserInfoResponseContext context = userInfoResponseContextLookupStrategy.apply(prc);
+ if (context == null) {
+ return false;
+ }
+ return doTest(prc, context);
+
+ }
+
+ /**
+ * Implementations should override this method to provide necessary logic.
+ *
+ * @param prc the profile request context
+ * @param context the UserInfo response context
+ *
+ * @return the result of this test
+ */
+ protected abstract boolean doTest(@Nonnull final ProfileRequestContext prc,
+ @Nonnull final UserInfoResponseContext context);
+
+}
diff --git a/sp-oidc-api/src/main/java/net/shibboleth/sp/oidc/messaging/context/logic/UserInfoPlainResponseTypeCondition.java b/sp-oidc-api/src/main/java/net/shibboleth/sp/oidc/messaging/context/logic/UserInfoPlainResponseTypeCondition.java
new file mode 100644
index 0000000..03e227a
--- /dev/null
+++ b/sp-oidc-api/src/main/java/net/shibboleth/sp/oidc/messaging/context/logic/UserInfoPlainResponseTypeCondition.java
@@ -0,0 +1,59 @@
+/*
+ * Licensed 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.sp.oidc.messaging.context.logic;
+
+import java.util.function.Function;
+
+import javax.annotation.Nonnull;
+
+import org.opensaml.profile.context.ProfileRequestContext;
+
+import com.nimbusds.common.contenttype.ContentType;
+import com.nimbusds.openid.connect.sdk.UserInfoSuccessResponse;
+
+import net.shibboleth.oidc.profile.context.UserInfoResponseContext;
+
+/**
+ * Condition that returns true if the UserInfo response was an plain JSON type i.e. not a signed and or encrypted JWT.
+ */
+public class UserInfoPlainResponseTypeCondition extends AbstractUserInfoResponseTypeCondition {
+
+ /** Constructor.*/
+ public UserInfoPlainResponseTypeCondition() {
+ super();
+ }
+
+ /**
+ *
+ * Constructor.
+ *
+ * @param strategy the UserInfo response context lookup strategy to use.
+ */
+ public UserInfoPlainResponseTypeCondition(
+ @Nonnull final Function<ProfileRequestContext, UserInfoResponseContext> strategy) {
+ super(strategy);
+ }
+
+ @Override
+ protected boolean doTest(@Nonnull final ProfileRequestContext prc,
+ @Nonnull final UserInfoResponseContext context) {
+ final UserInfoSuccessResponse userInfo = context.getUserInfo();
+ if (userInfo == null) {
+ return false;
+ }
+ return userInfo.getEntityContentType() == ContentType.APPLICATION_JSON &&
+ userInfo.getUserInfo() != null && userInfo.getUserInfoJWT() == null;
+ }
+}
diff --git a/sp-oidc-conf-impl/src/main/resources/META-INF/net.shibboleth.idp/postconfig.xml b/sp-oidc-conf-impl/src/main/resources/META-INF/net.shibboleth.idp/postconfig.xml
index cc42e57..1a1057f 100644
--- a/sp-oidc-conf-impl/src/main/resources/META-INF/net.shibboleth.idp/postconfig.xml
+++ b/sp-oidc-conf-impl/src/main/resources/META-INF/net.shibboleth.idp/postconfig.xml
@@ -88,29 +88,6 @@
</bean>
- <!-- Functions use by the flow and global beans TODO: IS there a better place for these -->
-
- <bean id="shibboleth.ChildLookup.OIDCProviderMetadataContext"
- class="org.opensaml.messaging.context.navigate.ChildContextLookup"
- c:type="#{ T(net.shibboleth.oidc.metadata.context.OIDCProviderMetadataContext) }" />
-
- <bean id="shibboleth.ChildLookup.OIDCProviderMetadataContextFromOutbound" parent="shibboleth.Functions.Compose">
- <constructor-arg name="g">
- <ref bean="shibboleth.ChildLookup.OIDCProviderMetadataContext" />
- </constructor-arg>
- <constructor-arg name="f">
- <ref bean="shibboleth.ChildLookup.OIDCPeerEntityFromOutbound" />
- </constructor-arg>
- </bean>
-
- <bean id="shibboleth.ChildLookup.OIDCPeerEntityFromOutbound" parent="shibboleth.Functions.Compose"
- c:g-ref="shibboleth.ChildLookup.OIDCPeerEntityContext" c:f-ref="shibboleth.MessageContextLookup.Outbound" />
-
- <bean id="shibboleth.ChildLookup.OIDCPeerEntityContext"
- class="org.opensaml.messaging.context.navigate.ChildContextLookup"
- c:type="#{ T(net.shibboleth.oidc.profile.messaging.context.OIDCPeerEntityContext) }" />
-
-
<!-- OpenID Provider information resolver service beans. -->
<bean id="shibboleth.authn.oidc.rp.ProviderMetadataResolver"
class="net.shibboleth.oidc.metadata.impl.ReloadingProviderMetadataProvider"
diff --git a/sp-oidc-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/consumer/oidc/oidc-beans.xml b/sp-oidc-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/consumer/oidc/oidc-beans.xml
index d6bfeae..07fb82c 100644
--- a/sp-oidc-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/consumer/oidc/oidc-beans.xml
+++ b/sp-oidc-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/consumer/oidc/oidc-beans.xml
@@ -230,8 +230,7 @@
<property name="providerMetadataLookupStrategy">
<ref bean="shibboleth.ChildLookup.OIDCProviderMetadataFromPeerEntityContext" />
</property>
- </bean>
-
+ </bean>
</list>
</property>
</bean>
@@ -258,8 +257,8 @@
<util:list id="IDTokenClaimsValidators" value-type="net.shibboleth.oidc.jwt.claims.ClaimsValidator">
<ref bean="IDTokenRequiredClaimsValidator" />
- <ref bean="IssuerClaimsValidator" />
- <ref bean="AudienceClaimsValidator" />
+ <!-- <ref bean="IssuerClaimsValidator" /> -->
+ <!-- <ref bean="AudienceClaimsValidator" /> -->
<ref bean="AzpClaimRequiredValidator" />
<!-- ref bean="AzpClaimsValidator" /> -->
<ref bean="ExpiryClaimsValidator" />
@@ -365,7 +364,7 @@
<bean id="AtHashValidator" class="net.shibboleth.oidc.security.jwt.claims.impl.AccessTokenHashValidator"
p:allowMissing="%{sp.oidc.tokenresponse.allowMissingAtHash:true}"
- p:accessTokenLookupStrategy="#{getObject('%{sp.oidc.jwt.AccessTokenLookupStrategy;}') ?:
+ p:accessTokenLookupStrategy="#{getObject('%{sp.oidc.jwt.AccessTokenLookupStrategy:}') ?:
getObject('DefaultAccessTokenLookupStrategy')}"
p:joseHeaderLookupStrategy="#{getObject('%{sp.oidc.jwt.IDTokenJOSEHeaderLookupStrategy:}') ?:
getObject('DefaultIDTokenJOSEHeaderLookupStrategy')}"/>
@@ -405,4 +404,150 @@
c:authenticationRequestLookupStrategy-ref="shibboleth.ChildLookup.MessageLookup.Outbound.OIDCAuthenticationRequest"/> -->
<!-- - End ID Token Claims Validation -->
+
+ <bean id="CheckUserInfoRequiredCondition" class=" net.shibboleth.oidc.profile.config.logic.UserInfoLookupPredicate" />
+
+
+ <bean id="UserInfoEndpointLookup" scope="prototype"
+ class="net.shibboleth.sp.oidc.profile.impl.UserInfoEndpointLookup"
+ p:httpClient="#{getObject('%{sp.oidc.HttpClient:}') ?: getObject('shibboleth.InternalHttpClient')}"
+ p:httpClientSecurityParameters="#{getObject('%{sp.oidc.httpClientSecurityParameters:}')}"
+ p:httpResponseDecoderStrategy="#{getObject('%{sp.oidc.userInfoResponseDecoder:}') ?: getObject('DefaultUserInfoResponseDecoder')}"
+ p:httpRequestEncoderStrategy="#{getObject('%{sp.oidc.userInfoRequestEncoder:}') ?: getObject('DefaultUserInfoRequestEncoder')}" />
+
+ <bean id="DefaultUserInfoResponseDecoder" scope="prototype"
+ class="net.shibboleth.oidc.profile.decoding.impl.UserInfoResponseDecoder"
+ p:objectMapper="#{getObject('%{sp.oidc.jsonObjectMapper:}') ?: getObject('shibboleth.sp.oidc.DefaultJSONObjectMapper')}"/>
+
+ <bean id="DefaultUserInfoRequestEncoder" scope="prototype"
+ class="net.shibboleth.oidc.profile.encoding.impl.UserInfoRequestEncoder"
+ p:providerMetadataLookupStrategy-ref="shibboleth.ChildLookup.OIDCProviderMetadataContextFromInboundMessageContext"
+ p:oAuth2ClientAuthenticationContextLookupStrategy-ref="shibboleth.ChildLookupOrCreate.OAuth2ClientAuthenticationContextFromInboundMessageContext"/>
+
+ <bean id="CheckUserInfoPlainResponseTypeCondition"
+ class="net.shibboleth.sp.oidc.messaging.context.logic.UserInfoPlainResponseTypeCondition" />
+
+ <!-- This is a very simplified and hard coded version of the claims verification used for a JWT. Maybe look to replace -->
+ <bean id="ValidateUserInfoPlainResponseClaims" scope="prototype"
+ class="net.shibboleth.sp.oidc.profile.impl.ValidateUserInfoJSONObjectClaims"/>
+
+ <bean id="ProcessEndUserClaims" class="net.shibboleth.sp.oidc.profile.impl.ProcessEndUserClaims"
+ scope="prototype"
+ p:claimMergingStrategy="#{getObject('%{sp.oidc.claimMergingStrategy:}') ?: getObject('DefaultClaimMergingStrategy')}"
+ p:claimSanitizationStrategy="#{getObject('%{sp.oidc.claimSanitizationStrategy:}') ?: getObject('DefaultClaimSanitizationStrategy')}" />
+
+ <bean id="DefaultClaimMergingStrategy"
+ class="net.shibboleth.sp.oidc.profile.impl.DefaultClaimMergingStrategy" />
+
+ <bean id="DefaultClaimSanitizationStrategy"
+ class="net.shibboleth.sp.oidc.profile.impl.DefaultClaimSanitizationStrategy" />
+
+ <!-- Will populate the same security params context as the id_token, but overwrite the decryption config. -->
+ <bean id="PopulateUserInfoDecryptionParameters"
+ class="net.shibboleth.oidc.profile.impl.PopulateJWTDecryptionParameters" scope="prototype"
+ p:decryptionParametersResolver-ref="JWTDecryptionParametersResolver">
+ <property name="configurationLookupStrategy">
+ <bean id="UserInfoTokenDecryptionConfigurationLookup" scope="prototype"
+ class="net.shibboleth.oidc.profile.config.navigate.JWTDecryptionConfigurationLookupFunction"/>
+ </property>
+ </bean>
+
+ <bean id="DecryptUserInfoJWE" class="net.shibboleth.oidc.security.impl.DecryptJWE" scope="prototype">
+ <property name="jwtTokenLookupStrategy">
+ <bean class="net.shibboleth.sp.oidc.context.navigate.EncryptedUserInfoJWTLookupStrategy"/>
+ </property>
+ <property name="jwtUpdateStrategy">
+ <bean class="net.shibboleth.sp.oidc.context.navigate.UserInfoInUserInfoResponseContextUpdateStrategy" />
+ </property>
+ </bean>
+
+ <!--
+ Note, this is identical in setup to the id_token signature validation flow as they both use the same config and trust engine.
+ the only difference is the location of the JWT to validate. Maybe they could be merged. Also, the populate steps may or may not
+ have already been performed in the id_token validation depending on the activation condition, so maybe those could be consolidated.
+ -->
+ <bean id="UserInfoTokenSignatureValidation" parent="WebFlowInboundMessageHandlerAdaptor" scope="prototype">
+ <constructor-arg>
+ <bean class="org.opensaml.messaging.handler.impl.BasicMessageHandlerChain">
+ <property name="handlers">
+ <list>
+
+ <bean scope="prototype" class="net.shibboleth.oidc.profile.impl.PopulateJWTSignatureValidationParametersHandler">
+ <property name="signatureValidationParametersResolver">
+ <bean class="net.shibboleth.oidc.security.jose.impl.BasicSignatureValidationParametersResolver" />
+ </property>
+ <property name="configurationLookupStrategy">
+ <bean class="net.shibboleth.oidc.profile.config.navigate.MessageContextLookupFunctionAdaptor">
+ <constructor-arg>
+ <bean class="net.shibboleth.oidc.profile.config.navigate.JWTSignatureValidationConfigurationLookupFunction"/>
+ </constructor-arg>
+ </bean>
+ </property>
+ </bean>
+
+ <!-- <bean
+ class="net.shibboleth.idp.plugin.authn.oidc.rp.metadata.impl.OIDCProviderMetadataLookupHandler"
+ scope="prototype" p:copyContextStrategy-ref="shibboleth.ChildLookup.OutboundOIDCMetadataContextLookup"
+ p:providerMetadataResolver-ref="shibboleth.authn.oidc.rp.ProviderMetadataResolver" /> -->
+
+ <bean class="net.shibboleth.oidc.security.impl.JWTMessageSignatureSecurityHandler"
+ scope="prototype">
+ <property name="jwtTokenLookupStrategy">
+ <bean
+ class="net.shibboleth.profile.context.navigate.SpringExpressionContextLookupFunction"
+ c:_0="#{ T(org.opensaml.messaging.context.MessageContext) }"
+ c:outputType="#{T(com.nimbusds.jwt.SignedJWT)}"
+ c:expression="#input.getSubcontext(T(net.shibboleth.oidc.profile.context.UserInfoResponseContext)).getUserInfo().getUserInfoJWT()" />
+ </property>
+ <property name="providerMetadataLookupStrategy">
+ <ref bean="shibboleth.ChildLookup.OIDCProviderMetadataFromPeerEntityContext" />
+ </property>
+ </bean>
+ </list>
+ </property>
+ </bean>
+ </constructor-arg>
+ <property name="errorEvent">
+ <util:constant static-field="org.opensaml.profile.action.EventIds.INVALID_MESSAGE" />
+ </property>
+ </bean>
+
+ <bean id="ValidateUserInfoTokenClaims" scope="prototype"
+ class="net.shibboleth.sp.oidc.profile.impl.ValidateTokenClaims"
+ p:cleanupHook="#{getObject('%{sp.oidc.userinfo.jwt.claims.CleanUpHook:}')}"
+ p:claimsValidator="#{getObject('%{sp.oidc.userinfo.userInfoTokenClaimsValidator:}')
+ ?: getObject('DefaultUserInfoTokenClaimsValidator')}"
+ p:jwtLookupStrategy="#{getObject('%{sp.oidc.userinfo.userInfoTokenLookupStrategy:}')
+ ?: getObject('DefaultUserInfoTokenLookupStrategy')}" />
+
+ <bean id="DefaultUserInfoTokenClaimsValidator"
+ class="net.shibboleth.oidc.security.jwt.claims.impl.ChainingJWTClaimsValidator"
+ p:claimValidators-ref="UserInfoClaimsValidators" />
+
+ <bean id="DefaultUserInfoTokenLookupStrategy"
+ class="net.shibboleth.sp.oidc.context.navigate.UserInfoJWTLookupStrategy" />
+
+ <util:list id="UserInfoClaimsValidators" value-type="net.shibboleth.oidc.jwt.claims.ClaimsValidator">
+ <ref bean="UserInfoTokenRequiredClaimsValidator" />
+ <ref bean="SubMatchesIDTokenClaimValidator" />
+ <!-- <ref bean="IssuerClaimsValidator" />
+ <ref bean="AudienceClaimsValidator" /> -->
+ </util:list>
+
+ <bean id="UserInfoTokenRequiredClaimsValidator"
+ class="net.shibboleth.oidc.security.jwt.claims.impl.RequiredClaimsValidator">
+ <property name="requiredClaims">
+ <list>
+ <value>sub</value>
+ </list>
+ </property>
+ </bean>
+
+ <bean id="SubMatchesIDTokenClaimValidator"
+ class="net.shibboleth.oidc.security.jwt.claims.impl.ExactMatchClaimsValidator" p:claimName="sub">
+ <property name="valueToMatchLookupStrategy">
+ <bean class="net.shibboleth.oidc.security.jwt.claims.impl.SubFromIDTokenLookupFunction" />
+ </property>
+ </bean>
+
</beans>
diff --git a/sp-oidc-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/consumer/oidc/oidc-flow.xml b/sp-oidc-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/consumer/oidc/oidc-flow.xml
index 1e054fe..afd13c1 100644
--- a/sp-oidc-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/consumer/oidc/oidc-flow.xml
+++ b/sp-oidc-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/consumer/oidc/oidc-flow.xml
@@ -50,57 +50,57 @@
<evaluate expression="PopulateIDTokenDecryptionParameters" />
<evaluate expression="DecryptIDTokenJWE" />
<!--Validation of the JWT signature is optional if TLS server validation was performed -->
- <!-- <evaluate expression="IDTokenSignatureValidation" /> -->
+ <evaluate expression="IDTokenSignatureValidation" />
<evaluate expression="ValidateIDTokenClaims" />
<!-- <evaluate expression="TokenResponsePopulateAuditContext" /> -->
<evaluate expression="'proceed'" />
- <transition on="proceed" to="proceed" />
+ <transition on="proceed" to="CheckUserInfoClaimsRequired" />
</action-state>
<!-- Should we use the access_token to request UserInfo claims? -->
<decision-state id="CheckUserInfoClaimsRequired">
- <if test="CheckUserInfoRequiredCondition.test(ProxyProfileRequestContextLookup.apply(opensamlProfileRequestContext))"
+ <if test="CheckUserInfoRequiredCondition.test(opensamlProfileRequestContext)"
then="UserInfoRequest" else="FinalizeResponse" />
</decision-state>
<action-state id="UserInfoRequest">
-<!-- <evaluate expression="UserInfoEndpointLookup" /> -->
+ <evaluate expression="UserInfoEndpointLookup" />
<evaluate expression="'proceed'" />
<transition on="proceed" to="CheckUserInfoResponseType" />
</action-state>
<decision-state id="CheckUserInfoResponseType">
- <if test="CheckUserInfoPlainResponseTypeCondition.test(ProxyProfileRequestContextLookup.apply(opensamlProfileRequestContext))"
+ <if test="CheckUserInfoPlainResponseTypeCondition.test(opensamlProfileRequestContext)"
then="ValidateUserInfoPlainClaimsSet" else="ValidateUserInfoJWT" />
</decision-state>
<!-- Actions to perform if the UserInfo response is a JWT type -->
<action-state id="ValidateUserInfoJWT">
-<!-- <evaluate expression="PopulateUserInfoDecryptionParameters" />
+ <evaluate expression="PopulateUserInfoDecryptionParameters" />
<evaluate expression="DecryptUserInfoJWE" />
<evaluate expression="UserInfoTokenSignatureValidation" />
<evaluate expression="ValidateUserInfoTokenClaims" />
- <evaluate expression="PostJWTUserInfoResponsePopulateAuditContext" /> -->
+ <!-- <evaluate expression="PostJWTUserInfoResponsePopulateAuditContext" /> -->
<evaluate expression="'proceed'" />
<transition on="proceed" to="FinalizeResponse" />
</action-state>
<!-- Plain UserInfo response types will skip straight to this stage -->
<action-state id="ValidateUserInfoPlainClaimsSet">
-<!-- <evaluate expression="ValidateUserInfoPlainResponseClaims" />
- <evaluate expression="PostPlainUserInfoResponsePopulateAuditContext" /> -->
+ <evaluate expression="ValidateUserInfoPlainResponseClaims" />
+ <!-- <evaluate expression="PostPlainUserInfoResponsePopulateAuditContext" /> -->
<evaluate expression="'proceed'" />
<transition on="proceed" to="FinalizeResponse" />
</action-state>
<action-state id="FinalizeResponse">
-<!-- <evaluate expression="ProcessEndUserClaims" />
- <evaluate expression="PostResponsePopulateAuditContext" />
- <evaluate expression="ValidateOIDCAuthentication" />
- <evaluate expression="PopulateSubjectCanonicalizationContext" />
+ <evaluate expression="ProcessEndUserClaims" />
+ <!-- <evaluate expression="PostResponsePopulateAuditContext" />-->
+ <!-- <evaluate expression="ValidateOIDCAuthentication" /> -->
+ <!-- <evaluate expression="PopulateSubjectCanonicalizationContext" />
<evaluate expression="WriteAuditLog" /> -->
<evaluate expression="'proceed'" />
- <transition on="proceed" to="CallSubjectCanonicalization" />
+ <transition on="proceed" to="proceed" />
</action-state>
diff --git a/sp-oidc-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/oidc-common-beans.xml b/sp-oidc-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/oidc-common-beans.xml
index 04caae8..920bfcf 100644
--- a/sp-oidc-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/oidc-common-beans.xml
+++ b/sp-oidc-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/oidc-common-beans.xml
@@ -132,4 +132,13 @@
</constructor-arg>
</bean>
+ <bean id="shibboleth.ChildLookup.OIDCProviderMetadataContextFromOutbound" parent="shibboleth.Functions.Compose">
+ <constructor-arg name="g">
+ <ref bean="shibboleth.ChildLookup.OIDCProviderMetadataContext" />
+ </constructor-arg>
+ <constructor-arg name="f">
+ <ref bean="shibboleth.ChildLookup.OIDCPeerEntityFromOutbound" />
+ </constructor-arg>
+ </bean>
+
</beans>
diff --git a/sp-oidc-conf-impl/src/main/resources/META-INF/net/shibboleth/sp/service/agent/postconfig.xml b/sp-oidc-conf-impl/src/main/resources/META-INF/net/shibboleth/sp/service/agent/postconfig.xml
index fbb3ec0..27924fd 100644
--- a/sp-oidc-conf-impl/src/main/resources/META-INF/net/shibboleth/sp/service/agent/postconfig.xml
+++ b/sp-oidc-conf-impl/src/main/resources/META-INF/net/shibboleth/sp/service/agent/postconfig.xml
@@ -35,8 +35,8 @@
<bean id="shibboleth.oidc.RemoteJwkSetCache"
class="net.shibboleth.oidc.jwk.RemoteJwkSetCache"
p:storage-ref="#{'%{idp.oidc.jwk.StorageService:shibboleth.StorageService}'.trim()}"
- p:httpClient="#{getObject('shibboleth.oidc.NonBrowser.HttpClient') ?: getObject('shibboleth.InternalHttpClient')}"
- p:httpClientSecurityParameters="#{getObject('shibboleth.oidc.NonBrowser.HttpClientSecurityParameters')}" />
+ p:httpClient="#{getObject('%{sp.oidc.HttpClient:}') ?: getObject('shibboleth.InternalHttpClient')}"
+ p:httpClientSecurityParameters="#{getObject('%{sp.oidc.httpClientSecurityParameters:}')}" />
<!--
Security Configuration Defaults. These settings establish the default security configurations for
@@ -255,11 +255,6 @@
class="net.shibboleth.oidc.security.credential.impl.ChainingJOSEObjectCredentialResolver">
<constructor-arg>
<list>
- <!-- Used by the OP -->
- <bean id="ClientInformationCredentialResolver"
- class="net.shibboleth.oidc.security.credential.impl.ClientInformationCredentialResolver"
- c:remoteJwkSetCache-ref="shibboleth.oidc.RemoteJwkSetCache"
- c:keyFetchInterval="%{idp.oidc.provider.keyfetch.interval:PT30M}"/>
<!-- Used by the RP -->
<bean id="OIDCProviderMetadataCredentialResolver"
class="net.shibboleth.oidc.security.credential.impl.ProviderMetadataCredentialResolver"
@@ -275,11 +270,6 @@
class="net.shibboleth.oidc.security.credential.impl.ChainingJOSEObjectCredentialResolver">
<constructor-arg>
<list>
- <!-- Used by the OP -->
- <bean id="ClientInformationCredentialResolver"
- class="net.shibboleth.oidc.security.credential.impl.ClientInformationCredentialResolver"
- c:remoteJwkSetCache-ref="shibboleth.oidc.RemoteJwkSetCache"
- c:keyFetchInterval="%{idp.oidc.provider.keyfetch.interval:PT30M}"/>
<!-- Used by the RP -->
<bean id="ClientSecretCriterionCredentialResolver"
class="net.shibboleth.oidc.security.credential.impl.ClientSecretCriterionCredentialResolver"/>
@@ -307,11 +297,6 @@
c:credentials-ref="shibboleth.oidc.EncryptionCredentials" />
</constructor-arg>
</bean>
- <!-- Used by the OP -->
- <bean id="ClientInformationCredentialResolver"
- class="net.shibboleth.oidc.security.credential.impl.ClientInformationCredentialResolver"
- c:remoteJwkSetCache-ref="shibboleth.oidc.RemoteJwkSetCache"
- c:keyFetchInterval="%{idp.oidc.provider.keyfetch.interval:PT30M}"/>
<!-- Used by the RP -->
<bean id="CriterionCredentialResolver"
class="net.shibboleth.oidc.security.credential.impl.ClientSecretCriterionCredentialResolver"/>
@@ -323,11 +308,6 @@
class="net.shibboleth.oidc.security.credential.impl.ChainingJOSEObjectCredentialResolver">
<constructor-arg>
<list>
- <!-- Used by the OP -->
- <bean id="ClientInformationCredentialResolver"
- class="net.shibboleth.oidc.security.credential.impl.ClientInformationCredentialResolver"
- c:remoteJwkSetCache-ref="shibboleth.oidc.RemoteJwkSetCache"
- c:keyFetchInterval="%{idp.oidc.provider.keyfetch.interval:PT30M}"/>
<!-- Used by the RP -->
<bean id="CriterionCredentialResolver"
class="net.shibboleth.oidc.security.credential.impl.ClientSecretCriterionCredentialResolver"/>
@@ -354,11 +334,6 @@
class="net.shibboleth.oidc.security.credential.impl.ChainingJOSEObjectCredentialResolver">
<constructor-arg>
<list>
- <!-- Used by the OP -->
- <bean id="ClientInformationCredentialResolver"
- class="net.shibboleth.oidc.security.credential.impl.ClientInformationCredentialResolver"
- c:remoteJwkSetCache-ref="shibboleth.oidc.RemoteJwkSetCache"
- c:keyFetchInterval="%{idp.oidc.provider.keyfetch.interval:PT30M}"/>
<!-- Used by the RP -->
<bean id="OIDCProviderMetadataCredentialResolver"
class="net.shibboleth.oidc.security.credential.impl.ProviderMetadataCredentialResolver"
diff --git a/sp-oidc-conf-impl/src/test/java/net/shibboleth/sp/oidc/flows/OIDCTokenConsumerFlowTest.java b/sp-oidc-conf-impl/src/test/java/net/shibboleth/sp/oidc/flows/OIDCTokenConsumerFlowTest.java
index 899f49b..a088c9e 100644
--- a/sp-oidc-conf-impl/src/test/java/net/shibboleth/sp/oidc/flows/OIDCTokenConsumerFlowTest.java
+++ b/sp-oidc-conf-impl/src/test/java/net/shibboleth/sp/oidc/flows/OIDCTokenConsumerFlowTest.java
@@ -14,6 +14,7 @@
package net.shibboleth.sp.oidc.flows;
+import static org.testng.Assert.assertNotNull;
import static org.testng.Assert.fail;
import java.io.ByteArrayInputStream;
@@ -21,6 +22,7 @@ import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.charset.StandardCharsets;
+import java.text.ParseException;
import java.time.Instant;
import java.util.Date;
import java.util.HashSet;
@@ -30,24 +32,19 @@ import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import org.apache.hc.client5.http.classic.HttpClient;
-import org.apache.hc.core5.http.ClassicHttpRequest;
+import org.apache.hc.core5.http.ClassicHttpResponse;
+import org.apache.hc.core5.http.ContentType;
+import org.apache.hc.core5.http.HttpHost;
import org.apache.hc.core5.http.io.HttpClientResponseHandler;
+import org.apache.hc.core5.http.io.entity.StringEntity;
import org.apache.hc.core5.http.protocol.HttpContext;
import org.mockito.Mockito;
import org.opensaml.core.xml.XMLObject;
-import org.opensaml.core.xml.io.MarshallingException;
import org.opensaml.core.xml.io.UnmarshallingException;
import org.opensaml.core.xml.util.XMLObjectSupport;
import org.opensaml.profile.context.ProfileRequestContext;
import org.opensaml.saml.saml2.core.NameID;
import org.opensaml.saml.saml2.core.NameIDType;
-import org.opensaml.security.SecurityException;
-import org.opensaml.security.credential.Credential;
-import org.opensaml.xmlsec.SignatureSigningParameters;
-import org.opensaml.xmlsec.signature.SignableXMLObject;
-import org.opensaml.xmlsec.signature.support.SignatureConstants;
-import org.opensaml.xmlsec.signature.support.SignatureException;
-import org.opensaml.xmlsec.signature.support.SignatureSupport;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.ApplicationContext;
@@ -58,10 +55,10 @@ import org.testng.Assert;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;
+import com.nimbusds.jose.JOSEException;
import com.nimbusds.jose.JWSAlgorithm;
-import com.nimbusds.jose.JWSHeader;
+import com.nimbusds.jwt.JWT;
import com.nimbusds.jwt.JWTClaimsSet;
-import com.nimbusds.jwt.SignedJWT;
import com.nimbusds.oauth2.sdk.AuthorizationCode;
import com.nimbusds.oauth2.sdk.ResponseMode;
import com.nimbusds.oauth2.sdk.id.State;
@@ -70,9 +67,13 @@ import com.nimbusds.oauth2.sdk.token.BearerAccessToken;
import com.nimbusds.oauth2.sdk.token.RefreshToken;
import com.nimbusds.openid.connect.sdk.AuthenticationSuccessResponse;
import com.nimbusds.openid.connect.sdk.OIDCTokenResponse;
+import com.nimbusds.openid.connect.sdk.UserInfoSuccessResponse;
+import com.nimbusds.openid.connect.sdk.claims.UserInfo;
import com.nimbusds.openid.connect.sdk.token.OIDCTokens;
+import net.minidev.json.JSONObject;
import net.shibboleth.idp.authn.AuthnEventIds;
+import net.shibboleth.oidc.security.credential.JWKCredential;
import net.shibboleth.shared.codec.Base64Support;
import net.shibboleth.shared.codec.DecodingException;
import net.shibboleth.shared.collection.CollectionSupport;
@@ -97,36 +98,22 @@ import net.shibboleth.sp.profile.ConsumerConstants;
}
)
@WebAppConfiguration
-public class OIDCTokenConsumerFlowTest extends AbstractSPFlowTest {
-
- /** Flow ID. */
- @Nonnull public static final String FLOW_ID = "sp/token-consumer";
-
- /** Issuer. */
- @Nonnull public static final String ISSUER = "https://op.example.org";
-
- /** Audience. */
- @Nonnull public static final String AUDIENCE = "https://testsp.example.org";
-
- /** REDIRECT URI. */
- @Nonnull public static final String RESPONSE_URL = "https://sp.example.org/Shibboleth.sso/callback";
-
- /** Resource URL. */
- @Nonnull public static final String RESOURCE_URL = "https://sp.example.org/secure";
+ at SuppressWarnings({ "unchecked", "rawtypes", "null" })
+public class OIDCTokenConsumerFlowTest extends AbstractSPFlowTest {
@Autowired
@Qualifier("shibboleth.SessionIDGenerator")
protected IdentifierGenerationStrategy idGenerator;
- /** Dummy signing key. */
- @Autowired @Qualifier("dummy.idp.Credential") protected Credential idpCredential;
+ /** Dummy signing key of the dummy OP we are testing against. */
+ @Autowired @Qualifier("dummy.op.signing.Credential") protected JWKCredential opSigningCredential;
/** The mocked HttpClient to use when responding to Token and UserInfo requests.*/
private HttpClient httpClient;
/** Constructor. */
public OIDCTokenConsumerFlowTest() {
- super(FLOW_ID);
+ super(TestConstants.FLOW_ID);
}
/** Pre-test work. */
@@ -144,66 +131,65 @@ public class OIDCTokenConsumerFlowTest extends AbstractSPFlowTest {
}
}
+
/**
- * Construct a successful OIDC token response.
+ * Test successful flow.
*
- * @param expiry expiry time
- * @param issuedAt issue time
- * @return the tokens
+ * @throws IOException
*/
- // TODO need to sign the ID token
- private OIDCTokens constructSuccessfulTokenResponse(@Nonnull final Instant expiry, @Nonnull final Instant issuedAt) {
+ @SuppressWarnings({ "unchecked", "rawtypes" })
+ @Test
+ public void testSuccess_PlainUserInfo() throws Exception {
+
+ mockOIDCEndpoints(constructSuccessfulTokenResponse(Instant.now().plusSeconds(3600), Instant.now()),
+ constructJSONUserInfoResponse());
- final JWTClaimsSet claimsSet = new JWTClaimsSet.Builder()
- .subject("fake-user")
- .issuer("https://op.example.org")
- .audience("mock-client-id")
- .expirationTime(Date.from(expiry))
- .issueTime(Date.from(issuedAt))
- .build();
+ final AuthenticationSuccessResponse response =
+ buildOIDCAuthorizationCodeResponse(TestConstants.RESPONSE_URL, ResponseMode.QUERY, TestConstants.STATE_STRING);
+ final DDF input = buildRemotedQueryStringResponse(response);
- final SignedJWT idToken = new SignedJWT(new JWSHeader(JWSAlgorithm.RS256), claimsSet);
- final AccessToken accessToken = new BearerAccessToken("fake-access-token-value", 3600, null);
- final RefreshToken refreshToken = new RefreshToken("fake-refresh-token-value");
- return new OIDCTokens(idToken, accessToken, refreshToken);
+ // Add cookies
+ input.addmember("http.headers.Cookie")
+ .unsafe_string(TestConstants.COOKIE_HEADER.getBytes("UTF-8"));
+
+ setApplicationRequest("test-oidc-application-with-ro", input);
+
+ final FlowExecutionResult result = flowExecutor.launchExecution(TestConstants.FLOW_ID, null, externalContext);
+ assertFlowExecutionResult(result, TestConstants.FLOW_ID);
+ assertFlowExecutionOutcome(result.getOutcome());
+ final DDF output = assertOutputMessageSuccess(result);
+ assert output != null;
+ System.out.println("testSuccess output: " + output.toString());
+ validateOutputMessage(result, CollectionSupport.singleton("mail"), TestConstants.RESOURCE_URL);
}
-
/**
* Test successful flow.
*
* @throws IOException
*/
- @SuppressWarnings({ "unchecked", "rawtypes" })
@Test
- public void testSuccess() throws IOException {
+ public void testSuccess_JWTUserInfo() throws Exception {
- // Create a mocked successful token response
- final OIDCTokenResponse accessTokenResponse = new OIDCTokenResponse(
- constructSuccessfulTokenResponse(Instant.now().plusSeconds(3600), Instant.now()));
- Mockito.when(httpClient.execute((ClassicHttpRequest) Mockito.any(), (HttpContext) Mockito.any(),
- (HttpClientResponseHandler) Mockito.any())).thenReturn(accessTokenResponse);
-
+ mockOIDCEndpoints(constructSuccessfulTokenResponse(Instant.now().plusSeconds(3600), Instant.now()),
+ constructJWTUserInfoResponse());
final AuthenticationSuccessResponse response =
- buildOIDCAuthorizationCodeResponse(RESPONSE_URL, ResponseMode.QUERY,
- "eyJzdGF0ZSI6IjE3NjEzMTY5Njc3MTBfMTYyMmE1YzcyNmRhOGY3YjM2ZTI0ZjE5ZWVkODJhZWEiLCJub25jZSI6ImYyNmQ5MjIyMjEyMjZjZDE4MzcyOWJmMTMyNDdkYmUyIn0");
-
+ buildOIDCAuthorizationCodeResponse(TestConstants.RESPONSE_URL, ResponseMode.QUERY, TestConstants.STATE_STRING);
final DDF input = buildRemotedQueryStringResponse(response);
// Add cookies
- input.addmember("http.headers.Cookie")
- .unsafe_string("__Host-_shibsp_req_1761316967710_1622a5c726da8f7b36e24f19eed82aea=f26d922221226cd183729bf13247dbe2; _Host-shibsp_state__test-oidc-application-with-ro_1761316967710_1622a5c726da8f7b36e24f19eed82aea=aHR0cHM6Ly9zcC5leGFtcGxlLm9yZy9zZWN1cmU; __Host-_shibsp_iss_1761316967710_1622a5c726da8f7b36e24f19eed82aea=https%3A%2F%2Fop.example.org;".getBytes("UTF-8"));
+ input.addmember("http.headers.Cookie").unsafe_string(TestConstants.COOKIE_HEADER.getBytes("UTF-8"));
setApplicationRequest("test-oidc-application-with-ro", input);
- final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
- assertFlowExecutionResult(result, FLOW_ID);
+ final FlowExecutionResult result = flowExecutor.launchExecution(TestConstants.FLOW_ID, null, externalContext);
+ assertFlowExecutionResult(result, TestConstants.FLOW_ID);
assertFlowExecutionOutcome(result.getOutcome());
final DDF output = assertOutputMessageSuccess(result);
assert output != null;
System.out.println("testSuccess output: " + output.toString());
- validateOutputMessage(result, CollectionSupport.singleton("mail"), RESOURCE_URL);
+ validateOutputMessage(result, CollectionSupport.singleton("mail"), TestConstants.RESOURCE_URL);
}
/**
@@ -214,21 +200,118 @@ public class OIDCTokenConsumerFlowTest extends AbstractSPFlowTest {
@Test
public void testFail_BadState() throws IOException {
final AuthenticationSuccessResponse response =
- buildOIDCAuthorizationCodeResponse(RESPONSE_URL, ResponseMode.QUERY, "eyJzdGF0ZSI6IjE3NjEzMTY5Njc3MTBfMTYyMmE1YzcyNmRhOGY3YjM2ZTI0ZjE5ZWVkODJhZWEiLCJub25jZSI6ImYyNmQ5MjIyMjEyMjZjZDE4MzcyOWJmMTMyNDdkYmUyIn0");
-
+ buildOIDCAuthorizationCodeResponse(TestConstants.RESPONSE_URL, ResponseMode.QUERY, TestConstants.STATE_STRING);
final DDF input = buildRemotedQueryStringResponse(response);
// Add cookies
input.addmember("http.headers.Cookie")
- .unsafe_string("__Host-_shibsp_req_1761316967710_1622a5c726da8f7b36e24f19eed82aea=f26d922221226cd183729bf13247dbe2; _Host-shibsp_state__test-oidc-application-with-ro_1761316967710_1622a5c726da8f7b36e24f19eed82aea=aHR0cHM6Ly9zcC5leGFtcGxlLm9yZy9zZWN1cmU; __Host-_shibsp_iss_1761316967710_1622a5c726da8f7b36e24f19eed82aea=https%3A%2F%2Fop.example.org;".getBytes("UTF-8"));
+ .unsafe_string(TestConstants.COOKIE_HEADER_BAD_STATE.getBytes("UTF-8"));
setApplicationRequest("test-oidc-application-with-ro", input);
- final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
- assertFlowExecutionResult(result, FLOW_ID);
+ final FlowExecutionResult result = flowExecutor.launchExecution(TestConstants.FLOW_ID, null, externalContext);
+ assertFlowExecutionResult(result, TestConstants.FLOW_ID);
assertFlowExecutionOutcome(result.getOutcome());
assertOutputMessageEvent(result, AuthnEventIds.NO_CREDENTIALS);
}
+
+ /**
+ * Construct a successful OIDC token response.
+ *
+ * @param expiry expiry time
+ * @param issuedAt issue time
+ * @return the tokens
+ * @throws ParseException
+ * @throws JOSEException
+ */
+ private OIDCTokenResponse constructSuccessfulTokenResponse(
+ @Nonnull final Instant expiry, @Nonnull final Instant issuedAt) throws JOSEException, ParseException {
+
+ final JWTClaimsSet claimsSet = new JWTClaimsSet.Builder()
+ .subject("fake-user")
+ .issuer("https://op.example.org")
+ .audience("mock-client-id")
+ .expirationTime(Date.from(expiry))
+ .issueTime(Date.from(issuedAt))
+ .build();
+ final JWT signedIdToken =
+ TestTokenHelper.createJWT(claimsSet, JWSAlgorithm.RS256, null, null, opSigningCredential, null);
+
+ final AccessToken accessToken = new BearerAccessToken("fake-access-token-value", 3600, null);
+ final RefreshToken refreshToken = new RefreshToken("fake-refresh-token-value");
+ return new OIDCTokenResponse((new OIDCTokens(signedIdToken, accessToken, refreshToken)));
+ }
+
+ /**
+ * Construct a successful OIDC user info response in JSON format. application/json.
+ *
+ * @return the user info response
+ */
+ private UserInfoSuccessResponse constructJSONUserInfoResponse() {
+ final JSONObject json = new JSONObject();
+ json.appendField("sub", "fake-user");
+ json.appendField("name", "Fake User");
+ return new UserInfoSuccessResponse(new UserInfo(json));
+ }
+
+ /**
+ * Construct a successful OIDC user info response in JWT format.
+ *
+ * @return the user info response
+ * @throws ParseException
+ * @throws JOSEException
+ */
+ private UserInfoSuccessResponse constructJWTUserInfoResponse() throws JOSEException, ParseException {
+ final JWTClaimsSet claimsSet = new JWTClaimsSet.Builder()
+ .subject("fake-user")
+ .issuer("https://op.example.org")
+ .audience("mock-client-id")
+ .claim("name", "Fake User").build();
+
+ final JWT signedUserInfoToken =
+ TestTokenHelper.createJWT(claimsSet, JWSAlgorithm.RS256, null, null, opSigningCredential, null);
+ return new UserInfoSuccessResponse(signedUserInfoToken);
+ }
+
+ /**
+ * Mock OIDC Token, UserInfo, and Keys endpoints. Returning the supplied responses.
+ * Noting these mocks may not exercise the response decoders, as the responses are returned directly.
+ *
+ * @param tokenResponse the token response
+ * @param userInfoResponse the user info response
+ *
+ * @throws IOException on error.
+ */
+ private void mockOIDCEndpoints(final OIDCTokenResponse tokenResponse,
+ final UserInfoSuccessResponse userInfoResponse) throws IOException {
+
+ Mockito.when(httpClient.execute(
+ Mockito.argThat(req -> req != null && req.getRequestUri().toString().contains("/token")),
+ Mockito.any(HttpContext.class),
+ Mockito.any(HttpClientResponseHandler.class)))
+ .thenReturn(tokenResponse);
+
+ Mockito.when(httpClient.execute(
+ Mockito.argThat(req -> req != null && req.getRequestUri().toString().contains("/userinfo")),
+ Mockito.any(HttpContext.class),
+ Mockito.any(HttpClientResponseHandler.class)))
+ .thenReturn(userInfoResponse);
+
+ // Fetch from the keys endpoint mocking the call used by the RemoteJwkSetCache
+ final String jwk = TestTokenHelper.createJWKJSONStringFrom(opSigningCredential);
+ assertNotNull(jwk);
+ final ClassicHttpResponse classicHttpResponse = Mockito.mock(ClassicHttpResponse.class);
+ Mockito.when(classicHttpResponse.getCode()).thenReturn(200);
+ Mockito.when(classicHttpResponse.getEntity())
+ .thenReturn(new StringEntity("{\"keys\":["+jwk+"]}", ContentType.APPLICATION_JSON));
+
+ Mockito.when(httpClient.executeOpen(
+ Mockito.nullable(HttpHost.class),
+ Mockito.argThat(req -> req != null && req.getRequestUri().toString().contains("/keys")),
+ Mockito.any(HttpContext.class)))
+ .thenReturn(classicHttpResponse);
+ }
+
/**
@@ -399,38 +482,16 @@ public class OIDCTokenConsumerFlowTest extends AbstractSPFlowTest {
throws IOException {
final DDF obj = new DDF(null).structure();
- obj.addmember(ConsumerConstants.BASE_URL).unsafe_string(RESOURCE_URL.getBytes(StandardCharsets.UTF_8));
+ obj.addmember(ConsumerConstants.BASE_URL).unsafe_string(TestConstants.RESOURCE_URL.getBytes(StandardCharsets.UTF_8));
final DDF http = obj.addmember(RemotedHttpServletRequest.STRUCTURE_NAME).structure();
http.addmember(RemotedHttpServletRequest.METHOD).string("GET");
http.addmember(RemotedHttpServletRequest.REMOTE_ADDR).string("192.168.1.1");
- http.addmember(RemotedHttpServletRequest.REQUEST_URL).unsafe_string(RESPONSE_URL.getBytes(StandardCharsets.UTF_8));
+ http.addmember(RemotedHttpServletRequest.REQUEST_URL).unsafe_string(TestConstants.RESPONSE_URL.getBytes(StandardCharsets.UTF_8));
final URI responseURI = response.toURI();
http.addmember(RemotedHttpServletRequest.QUERY_STRING).string(Constraint
.isNotNull(responseURI.getQuery(), "Query string is null"));
return obj;
}
-
- /**
- * Sign object.
- *
- * @param signable object to sign
- *
- * @throws IOException
- */
- public void sign(@Nonnull final SignableXMLObject signable) throws IOException {
-
- final SignatureSigningParameters signingParameters = new SignatureSigningParameters();
- signingParameters.setSigningCredential(idpCredential);
- signingParameters.setSignatureAlgorithm(SignatureConstants.ALGO_ID_SIGNATURE_RSA_SHA256);
- signingParameters.setSignatureCanonicalizationAlgorithm(SignatureConstants.ALGO_ID_C14N_EXCL_OMIT_COMMENTS);
-
- try {
- SignatureSupport.signObject(signable, signingParameters);
- } catch (final SecurityException | MarshallingException | SignatureException e) {
- throw new IOException(e);
- }
- }
-
}
\ No newline at end of file
diff --git a/sp-oidc-conf-impl/src/test/java/net/shibboleth/sp/oidc/flows/TestConstants.java b/sp-oidc-conf-impl/src/test/java/net/shibboleth/sp/oidc/flows/TestConstants.java
new file mode 100644
index 0000000..da9c9de
--- /dev/null
+++ b/sp-oidc-conf-impl/src/test/java/net/shibboleth/sp/oidc/flows/TestConstants.java
@@ -0,0 +1,74 @@
+/*
+ * Licensed 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.sp.oidc.flows;
+
+import javax.annotation.Nonnull;
+
+/** A class to hold constants used for tests.*/
+public final class TestConstants {
+
+ /** Private constructor.*/
+ private TestConstants() {
+
+ }
+
+ /** Flow ID. */
+ @Nonnull public static final String FLOW_ID = "sp/token-consumer";
+
+ /** Issuer. */
+ @Nonnull public static final String ISSUER = "https://op.example.org";
+
+ /** Audience. */
+ @Nonnull public static final String AUDIENCE = "https://testsp.example.org";
+
+ /** REDIRECT URI. */
+ @Nonnull public static final String RESPONSE_URL = "https://sp.example.org/Shibboleth.sso/callback";
+
+ /** Resource URL. */
+ @Nonnull public static final String RESOURCE_URL = "https://sp.example.org/secure";
+
+ /**
+ * A returned state String consisting of a state and a nonce:
+ *
+ * <code>
+ * {"state":"1761316967710_1622a5c726da8f7b36e24f19eed82aea",
+ * "nonce":"f26d922221226cd183729bf13247dbe2"}
+ * </code>
+ * */
+ public static final String STATE_STRING = """
+ eyJzdGF0ZSI6IjE3NjEzMTY5Njc3MTBfMTYyMmE1YzcyNmRhOGY3YjM2ZTI0ZjE5ZWVkODJhZWEiLCJub25jZSI6ImYyNmQ5MjIyMjEyMjZjZDE4MzcyOWJmMTMyNDdkYmUyIn0
+ """;
+
+ /**
+ * Cookie headers representing the state string {@literal STATE_STRING}:
+ *
+ * __Host-_shibsp_req_{state}={nonce};
+ * _Host-shibsp_state__{appId}_{state}={targetURL};
+ * __Host-_shibsp_iss_{state}={issuer};
+ *
+ */
+ public static final String COOKIE_HEADER = """
+ __Host-_shibsp_req_1761316967710_1622a5c726da8f7b36e24f19eed82aea=f26d922221226cd183729bf13247dbe2;
+ _Host-shibsp_state__test-oidc-application-with-ro_1761316967710_1622a5c726da8f7b36e24f19eed82aea=aHR0cHM6Ly9zcC5leGFtcGxlLm9yZy9zZWN1cmU;
+ __Host-_shibsp_iss_1761316967710_1622a5c726da8f7b36e24f19eed82aea=https%3A%2F%2Fop.example.org;
+ """;
+
+ //TODO this is not bad state
+ public static final String COOKIE_HEADER_BAD_STATE = """
+ __Host-_shibsp_req_1761316967710_1622a5c726da8f7b36e24f19eed82aea=f26d922221226cd183729bf13247dbe2;
+ _Host-shibsp_state__test-oidc-application-with-ro_1761316967710_1622a5c726da8f7b36e24f19eed82aea=aHR0cHM6Ly9zcC5leGFtcGxlLm9yZy9zZWN1cmU;
+ __Host-_shibsp_iss_1761316967710_1622a5c726da8f7b36e24f19eed82aea=https%3A%2F%2Fop.example.org;
+ """;
+}
diff --git a/sp-oidc-conf-impl/src/test/java/net/shibboleth/sp/oidc/flows/TestTokenHelper.java b/sp-oidc-conf-impl/src/test/java/net/shibboleth/sp/oidc/flows/TestTokenHelper.java
new file mode 100644
index 0000000..dcaffa9
--- /dev/null
+++ b/sp-oidc-conf-impl/src/test/java/net/shibboleth/sp/oidc/flows/TestTokenHelper.java
@@ -0,0 +1,349 @@
+/*
+ * Licensed 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.sp.oidc.flows;
+
+import java.security.PublicKey;
+import java.security.interfaces.ECPrivateKey;
+import java.security.interfaces.ECPublicKey;
+import java.security.interfaces.RSAPublicKey;
+import java.text.ParseException;
+import java.time.Instant;
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.security.credential.Credential;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.nimbusds.jose.EncryptionMethod;
+import com.nimbusds.jose.JOSEException;
+import com.nimbusds.jose.JOSEObjectType;
+import com.nimbusds.jose.JWEAlgorithm;
+import com.nimbusds.jose.JWEHeader;
+import com.nimbusds.jose.JWEObject;
+import com.nimbusds.jose.JWSAlgorithm;
+import com.nimbusds.jose.JWSHeader;
+import com.nimbusds.jose.Payload;
+import com.nimbusds.jose.crypto.AESEncrypter;
+import com.nimbusds.jose.crypto.DirectEncrypter;
+import com.nimbusds.jose.crypto.ECDHEncrypter;
+import com.nimbusds.jose.crypto.ECDSASigner;
+import com.nimbusds.jose.crypto.MACSigner;
+import com.nimbusds.jose.crypto.RSAEncrypter;
+import com.nimbusds.jose.crypto.RSASSASigner;
+import com.nimbusds.jose.jwk.KeyUse;
+import com.nimbusds.jose.jwk.RSAKey;
+import com.nimbusds.jwt.EncryptedJWT;
+import com.nimbusds.jwt.JWT;
+import com.nimbusds.jwt.JWTClaimsSet;
+import com.nimbusds.jwt.PlainJWT;
+import com.nimbusds.jwt.SignedJWT;
+
+import net.shibboleth.oidc.security.credential.JWKCredential;
+import net.shibboleth.shared.collection.CollectionSupport;
+
+/** A helper class for generating tokens for testing.*/
+public final class TestTokenHelper {
+
+ /** Private constructor.*/
+ private TestTokenHelper() {
+
+ }
+
+ /**
+ * Build a basic {@link JWTClaimsSet} from the supplied parameters.
+ *
+ * @param overrideClaims claims to override
+ *
+ * @return the JWT claims set.
+ */
+ @SuppressWarnings("null")
+ @Nonnull public static JWTClaimsSet createBasicClaims(final Map<String, Object> overrideClaims) {
+
+ return new JWTClaimsSet.Builder()
+ .issuer(getClaimValue(overrideClaims, "iss", "test-issuer", String.class))
+ .audience(getClaimValue(overrideClaims, "aud", List.of("test-client"), List.class))
+ .subject(getClaimValue(overrideClaims, "sub", "jdoe", String.class))
+ .claim("preferred_username", getClaimValue(overrideClaims, "preferred_username", "d.tu", String.class))
+ .claim("given_name", getClaimValue(overrideClaims, "given_name", "Demo", String.class))
+ .claim("family_name", getClaimValue(overrideClaims, "family_name", "User", String.class))
+ .claim("nonce", getClaimValue(overrideClaims, "nonce", "abadnonce", String.class))
+ .claim("nickname", getClaimValue(overrideClaims, "nickname", "Dee", String.class))
+ .claim("azp", getClaimValue(overrideClaims, "azp", "test-client", String.class))
+ .claim("name",getClaimValue(overrideClaims, "name", "Demo T. User", String.class))
+ .claim("acr",getClaimValue(overrideClaims, "acr", "urn:mace:incommon:iap:silver", String.class))
+ .claim("amr",getClaimValue(overrideClaims, "amr", List.of("pwd", "otp"), List.class))
+ .claim("auth_time", getClaimValue(overrideClaims, "auth_time", new Date(), Date.class))
+ .issueTime(getClaimValue(overrideClaims, "iat", new Date(), Date.class))
+ .expirationTime(getClaimValue(overrideClaims, "auth_time",
+ Date.from(Instant.now().plusSeconds(120)), Date.class))
+ .build();
+ }
+
+ /**
+ * Create a JWK JSON string from the supplied JWKCredential. Return an empty string if the key type is
+ * not supported.
+ *
+ * @param credential the JWK credential
+ * @return the JWK JSON string, or empty string if the key type is not supported.
+ */
+ @Nonnull public static String createJWKJSONStringFrom(final JWKCredential credential) {
+ final PublicKey publicKey = credential.getPublicKey();
+ if (publicKey instanceof final RSAPublicKey rsa) {
+ final RSAKey publicKeyJWK = new RSAKey.Builder(rsa)
+ .keyID(credential.getKid())
+ .algorithm(credential.getAlgorithm())
+ .keyUse(KeyUse.SIGNATURE)
+ .build();
+ return publicKeyJWK.toJSONString();
+ }
+ return "";
+ }
+
+ /**
+ * Build a basic {@link JWTClaimsSet} from the supplied parameters to mock a UserInfo response.
+ *
+ * @param overrideClaims claims to override
+ *
+ * @return the JWT claims set.
+ */
+ @SuppressWarnings("null")
+ @Nonnull public static JWTClaimsSet createBasicUserInfoClaims(final Map<String, Object> overrideClaims) {
+
+ return new JWTClaimsSet.Builder()
+ .issuer(getClaimValue(overrideClaims, "iss", "test-issuer", String.class))
+ .audience(getClaimValue(overrideClaims, "aud", CollectionSupport.listOf("test-client"), List.class))
+ .subject(getClaimValue(overrideClaims, "sub", "jdoe", String.class))
+ .claim("preferred_username", getClaimValue(overrideClaims, "preferred_username", "d.tu", String.class))
+ .claim("given_name", getClaimValue(overrideClaims, "given_name", "Demo", String.class))
+ .claim("family_name", getClaimValue(overrideClaims, "family_name", "User", String.class))
+ .claim("nickname", getClaimValue(overrideClaims, "nickname", "Dee", String.class))
+ .claim("name",getClaimValue(overrideClaims, "name", "Demo T. User", String.class))
+ .build();
+ }
+
+ /**
+ * Extract any override from the overrides map, or return the default value if there is none.
+ *
+ * @param <T> the type to look for and return
+ * @param overrideClaims the overrides map
+ * @param claimName the claim name
+ * @param defaultValue the default value if no override is found
+ * @param type the type to return
+ *
+ * @return the override or the default value.
+ */
+ private static <T> T getClaimValue(final Map<String, Object> overrideClaims, final String claimName,
+ final Object defaultValue, final Class<T> type) {
+
+ if (overrideClaims.containsKey(claimName)) {
+ final Object override = overrideClaims.get(claimName);
+ if (override == null) {
+ return null;
+ }
+ if (type.isInstance(override)) {
+ //check empty string case, set null if so
+ if (override instanceof String && ((String)override).isEmpty()) {
+ return null;
+ }
+ return type.cast(override);
+ }
+ }
+ return type.cast(defaultValue);
+ }
+
+ /**
+ * Create a JWT from the given payload. The JWT can either be plain, or signed and encrypted. If encrypted, it must
+ * be signed.
+ *
+ * @param payload the payload to create the JWT from
+ * @param sigAlg the signature alg to use. {@code Null} if the JWT is not going to be signed and/or encrypted.
+ * @param jweAlg the encryption alg to use. {@code Null} if the JWT is not going to be encrypted. Note, if encrypted
+ * the signature algorithm must also be supplied, otherwise a plain JWT will be returned.
+ * @param enc the content encryption algorithm to use.
+ * @param sigKey the key to use for signing
+ * @param encKey the key to use for encryption
+ *
+ * @return and plain JWT, JWS, or JWE.
+ *
+ * @throws JOSEException on error.
+ * @throws ParseException on error.
+ */
+ @Nonnull public static JWT createJWT(@Nonnull final JWTClaimsSet payload, @Nullable final JWSAlgorithm sigAlg,
+ @Nullable final JWEAlgorithm jweAlg, @Nullable final EncryptionMethod enc,
+ @Nullable final Credential sigKey, @Nullable final Credential encKey) throws JOSEException, ParseException {
+
+ // Sign first.
+ if (sigAlg != null && sigKey != null) {
+ final var header = new JWSHeader.Builder(sigAlg)
+ .type(JOSEObjectType.JWT)
+ .build();
+ final var signedJWT = new SignedJWT(header,payload);
+
+ if (JWSAlgorithm.Family.HMAC_SHA.contains(sigAlg)){
+ signedJWT.sign(new MACSigner(sigKey.getSecretKey()));
+ } else if (JWSAlgorithm.Family.RSA.contains(sigAlg)) {
+ signedJWT.sign(new RSASSASigner(sigKey.getPrivateKey()));
+ } else if (JWSAlgorithm.Family.EC.contains(sigAlg)) {
+ signedJWT.sign(new ECDSASigner((ECPrivateKey)sigKey.getPrivateKey()));
+ }
+
+ if (jweAlg != null && encKey != null) {
+ final JWEObject jweObject =
+ new JWEObject(new JWEHeader.Builder(jweAlg, enc)
+ .contentType("JWT")
+ .build(),
+ new Payload(signedJWT));
+
+ if (JWEAlgorithm.Family.RSA.contains(jweAlg)) {
+ jweObject.encrypt(new RSAEncrypter((RSAPublicKey)encKey.getPublicKey()));
+ final JWT encJwt = EncryptedJWT.parse(jweObject.serialize());
+ assert encJwt != null;
+ return encJwt;
+
+ } else if (JWEAlgorithm.Family.AES_KW.contains(jweAlg)
+ || JWEAlgorithm.Family.AES_GCM_KW.contains(jweAlg)) {
+
+ jweObject.encrypt(new AESEncrypter(encKey.getSecretKey()));
+ final JWT encJwt = EncryptedJWT.parse(jweObject.serialize());
+ assert encJwt != null;
+ return encJwt;
+
+ } else if (JWEAlgorithm.Family.ECDH_ES.contains(jweAlg)) {
+ jweObject.encrypt(new ECDHEncrypter((ECPublicKey) encKey.getPublicKey()));
+ final JWT encJwt = EncryptedJWT.parse(jweObject.serialize());
+ assert encJwt != null;
+ return encJwt;
+ } else if (JWEAlgorithm.DIR == jweAlg) {
+ jweObject.encrypt(new DirectEncrypter(encKey.getSecretKey()));
+ final JWT encJwt = EncryptedJWT.parse(jweObject.serialize());
+ assert encJwt != null;
+ return encJwt;
+ }
+ } else {
+ return signedJWT;
+ }
+ }
+ return new PlainJWT(payload);
+ }
+
+ /**
+ * Create a JWT UserInfo response. Can be a JWS or JWE depending on the input values.
+ * @param overrideClaims claims to override
+ * @param sigAlg the signature algorithm
+ * @param jweAlg the encryption algorithm
+ * @param enc the content encryption algorithm
+ * @param sigKey the signing key
+ * @param encKey the encryption key
+ *
+ * @return the created JWT
+ * @throws JOSEException on error
+ * @throws ParseException on error
+ */
+ @Nonnull public static JWT createJWTUserInfoResponse(final Map<String, Object> overrideClaims,
+ @Nullable final JWSAlgorithm sigAlg, @Nullable final JWEAlgorithm jweAlg,
+ @Nullable final EncryptionMethod enc, @Nullable final Credential sigKey, @Nullable final Credential encKey)
+ throws JOSEException, ParseException {
+
+ final var payload = createBasicUserInfoClaims(overrideClaims);
+ return createJWT(payload, sigAlg, jweAlg, enc, sigKey, encKey);
+
+ }
+
+ /**
+ * Create an access token response JSON object and build a suitable id_token to include in the response. Any claim
+ * contained in the overrides list overrides the default value built by the basic claims method.
+ *
+ * @param overrideClaims claims to override in the id_token
+ * @param sigAlg the signature algorithm
+ * @param jweAlg the encryption algorithm
+ * @param enc the content encryption algorithm
+ * @param sigKey the signing key
+ * @param encKey the encryption key
+ *
+ * @return an access token response JSON object
+ *
+ * @throws Exception on error.
+ */
+ @Nonnull public static String createAccessTokenResponseJSON(final Map<String, Object> overrideClaims,
+ @Nullable final JWSAlgorithm sigAlg,
+ @Nullable final JWEAlgorithm jweAlg, @Nullable final EncryptionMethod enc,
+ @Nullable final Credential sigKey, @Nullable final Credential encKey) throws Exception {
+
+
+ final var payload = createBasicClaims(overrideClaims);
+ final JWT idToken = createJWT(payload, sigAlg, jweAlg, enc, sigKey, encKey);
+
+ return buildTemplateAccessTokenJSONResponse(idToken.serialize());
+ }
+
+
+ /**
+ * Create a plain JSON object based UserInfo response.
+ *
+ * @param overrideClaims claims to override
+ *
+ * @return a plain JSON object representing the UserInfo response
+ *
+ * @throws JsonProcessingException on error
+ */
+ @Nonnull public static String createPlainUserInfoResponseString(final Map<String, Object> overrideClaims)
+ throws JsonProcessingException {
+ final JWTClaimsSet claims = createBasicUserInfoClaims(overrideClaims);
+ final String valueAsString = new ObjectMapper().writeValueAsString(claims.toJSONObject());
+ assert valueAsString != null;
+ return valueAsString;
+ }
+
+
+ /**
+ * Create a Plain UserInfo response JWT.
+ *
+ * @param overrideClaims claims to override
+ *
+ * @return the signed JWT
+ *
+ * @throws JOSEException on error
+ */
+ @Nonnull public static PlainJWT createPlainJWTUserInfoResponseJSON(final Map<String, Object> overrideClaims)
+ throws JOSEException {
+
+ final var payload = createBasicUserInfoClaims(overrideClaims);
+ return new PlainJWT(payload);
+ }
+
+
+ /**
+ * Build a simple OAuth2.0/OIDC Access Token JSON response using the serialized JWT.
+ *
+ * @param serializedJWT the id_token serialized
+ *
+ * @return An access token response
+ */
+ @Nonnull private static String buildTemplateAccessTokenJSONResponse(final String serializedJWT) {
+ return "{\n"
+ + " \"access_token\": \"W0y5aDNAzEPNpSzu1cuMG904BZuQFZJUUwG5F3ct0zydZWy1ji\",\n"
+ + " \"token_type\": \"Bearer\",\n"
+ + " \"id_token\": \""+serializedJWT+"\",\n"
+ + " \"scope\": \"openid\"\n"
+ + "}";
+ }
+
+}
diff --git a/sp-oidc-conf-impl/src/test/resources/metadata/openid-configuration.json b/sp-oidc-conf-impl/src/test/resources/metadata/openid-configuration.json
index 9433cbb..592e3b4 100644
--- a/sp-oidc-conf-impl/src/test/resources/metadata/openid-configuration.json
+++ b/sp-oidc-conf-impl/src/test/resources/metadata/openid-configuration.json
@@ -1,10 +1,10 @@
{
"issuer": "https://op.example.org",
-"authorization_endpoint": "https://op.example.org/o/oauth2/v2/auth",
+"authorization_endpoint": "https://op.example.org/auth",
"token_endpoint": "https://oauth2.op.example.org/token",
-"userinfo_endpoint": "https://openidconnect.op.example.org/v1/userinfo",
-"revocation_endpoint": "https://oauth2.op.example.org/revoke",
-"jwks_uri": "https://op.example.org/oauth2/v3/certs",
+"userinfo_endpoint": "https://op.example.org/userinfo",
+"revocation_endpoint": "https://op.example.org/revoke",
+"jwks_uri": "https://op.example.org/keys",
"request_parameter_supported" : true,
"request_object_signing_alg_values_supported" : ["RS256"],
"response_types_supported": [
diff --git a/sp-oidc-conf-impl/src/test/resources/net/shibboleth/idp/module/credentials/op/op-signing-rsa.jwk b/sp-oidc-conf-impl/src/test/resources/net/shibboleth/idp/module/credentials/op/op-signing-rsa.jwk
new file mode 100644
index 0000000..2f01609
--- /dev/null
+++ b/sp-oidc-conf-impl/src/test/resources/net/shibboleth/idp/module/credentials/op/op-signing-rsa.jwk
@@ -0,0 +1,14 @@
+{
+ "kty": "RSA",
+ "use": "sig",
+ "alg": "RS256",
+ "kid": "op-signing-key",
+ "d": "bnhwivAdsfkabz_kVXMoFDwcUCTtx5v8QtMhweQ_2WHIHV56ViOu7eYR6MmLV-yrLukgazOlxXGAUNV_ET_yjiTVvDFdpMynOUI2ZrFzoEdu9l7DVgOG5l-_CvKpKh4P14gLV0HawYSZ2aJEPaHP5NgIgYa9oMm4c_uVRzc-OiQAn7THeVInfeaXsDavCB-YdJmsuIfL-GWdbz8t3J6yGRuoVXtIx2b9rkFOWA6DLTuTRXYpBT92qfxk7s-e2x3KYpUr14sbPR68Jb7O48k5NmIEUcKdCysi9NDBAgwmzkHPXep0mf3S2arHd1u8J14nPe_HSkF9mY6pLhD_qaM4qQ",
+ "n": "oRDRa8TF-kZw1ezz7CHWmovmgnkdMl3-PTVvkzuX4MECDoyIkyczWH97C1poGBorjfvIHtKpBHjoeCJDCXW4Eeb8ZSRVpUbh_0GkIUJcy9xSIqMBv0XohhSaGun1zHFHEvEPi0E-ljx08mlwxq8C5Zuim3miNV8yaZEWTpjSgMyUdPSyBcaAQKMQnsawFsZaAyc528NwM14w0cQbd5dHRgnxilb7eHSOKEv0GzewxsKDlhGJ3m7h_wSpVieVqKnZ9j7wXlpXJD0X85bI7pBlEm4xUoRnojONfSUdoou2EDb7uheVfJRdkNuoSu6aKcL73ZJLu82Mofwcpstg3KL5nQ",
+ "e": "AQAB",
+ "p": "x5ClON9tpwcH4adEZ5jqMMJs8xSl7aVFNqzDPThrhlgOWevoP51GEPlqQENqwfqIrHJpuElC9vPeUdL0bK-fZ2FFjsnHstKZLBHXIVHMBQ5jY2f11mTc6d1e-mejKzvDn7tl-kRyrR4kxqABrR0MOSLZUzR3UBv6aRH3kl0ISO8",
+ "q": "zp0LPOHncNTVpDoMug-hlpaoBX-AK8sEFok2gsKLhTMFhyROdpZIPJHKYgSJijuqaL-pg1tIWWLFNdu5H_Vy-v3cGveM37UUNmUzQKxzeXnESD1HwoQPhD5mGDAu8RRY8nPkFfXEvQ9qKRW89rDqqdRSioIdl7WVP7yLd7A2rjM",
+ "dp": "kOl64Gp1sFTNp1ETvfpvDEsSXA0BVCivsr0O5NSkV6B3g1pnglIM_-RtAA80ZXksZ7SJCjXAt5xsMpRxwr5gjOw4WzAwn6yHZ1XpFpvT_1PsXdGu1SjWtcd24XQCKzGxorqsmWe8sfLyl3y22uL97jtc_itZ_ETkuAlA8fo2Ouc",
+ "dq": "Gi-JJxQycvhmg0PgLQyiHCXH8bpxEhvOztRhFj111VHcF36geyMc0KBhl_6fN-fL9y6jW0SMbNe5ytOoKMbA5dKavMg3EHswrpww3Ld-gxzNpaIuoBaAqPAHnHUu3dsIUuIhPC9D2fpEKCTbKCDP_Oot8-P9wCaBXeyVeHva0kU",
+ "qi": "DuuQsro41wxx5BZ-BbOpSmDbK60qkZa2jU3jcNe9V6AuhlInYo0-9QPyuETqzYyWd8oMGnLEd9-StXB7UOsK0pmAiv0R8ALIKci0a64z27Rw8wuQoMZMW1SOYS_zhG6p2iK2LXHy-rzr1PAKEGOEK7DA16zXFqwNqky2PNX-teg"
+}
\ No newline at end of file
diff --git a/sp-oidc-conf-impl/src/test/resources/net/shibboleth/sp/oidc-test-beans.xml b/sp-oidc-conf-impl/src/test/resources/net/shibboleth/sp/oidc-test-beans.xml
index caf31d8..4a969c3 100644
--- a/sp-oidc-conf-impl/src/test/resources/net/shibboleth/sp/oidc-test-beans.xml
+++ b/sp-oidc-conf-impl/src/test/resources/net/shibboleth/sp/oidc-test-beans.xml
@@ -19,23 +19,17 @@
<util:list id="test.sp.oidc.AgentResolverResources">
<value>%{idp.home}/conf/sp/oidc-test-agents.xml</value>
</util:list>
-
- <bean id="dummy.idp.X509Certificate" class="net.shibboleth.shared.spring.security.factory.X509CertificateFactoryBean"
- p:resource="%{idp.home}/credentials/idp-signing.crt" />
-
- <bean id="dummy.idp.PrivateKey" class="net.shibboleth.shared.spring.security.factory.PrivateKeyFactoryBean"
- p:resource="%{idp.home}/credentials/idp-signing.key" />
-
- <bean id="dummy.idp.Credential" class="org.opensaml.security.x509.BasicX509Credential"
- c:entityCertificate-ref="dummy.idp.X509Certificate"
- c:privateKey-ref="dummy.idp.PrivateKey"
- p:entityId="https://idp.example.org" />
<bean id="dummy.oidc.BasicEncryptionConfiguration" abstract="true"
class="net.shibboleth.oidc.security.jose.impl.BasicEncryptionConfiguration"
p:includedAlgorithms="#{getObject('shibboleth.oidc.IncludedEncryptionAlgorithms')}"
p:excludedAlgorithms="#{getObject('shibboleth.oidc.ExcludedEncryptionAlgorithms')}" />
+
+ <bean id="shibboleth.oidc.JWKCredential" abstract="true"
+ class="net.shibboleth.oidc.security.credential.BasicJWKCredentialFactoryBean" />
+ <bean id="dummy.op.signing.Credential" parent="shibboleth.oidc.JWKCredential"
+ p:resource="%{idp.home}/credentials/op/op-signing-rsa.jwk" p:throwIfNull="false" />
<!-- Mockito mock for HttpClient -->
<bean id="Mock.HttpClient"
diff --git a/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/AbstractAuthorizationResponseAction.java b/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/AbstractAuthorizationResponseAction.java
index 3730631..9ff06d6 100644
--- a/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/AbstractAuthorizationResponseAction.java
+++ b/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/AbstractAuthorizationResponseAction.java
@@ -30,11 +30,11 @@ import org.slf4j.Logger;
import com.nimbusds.openid.connect.sdk.AuthenticationResponse;
import com.nimbusds.openid.connect.sdk.AuthenticationSuccessResponse;
+import net.shibboleth.idp.profile.AbstractProfileAction;
import net.shibboleth.oidc.metadata.context.OIDCProviderMetadataContext;
import net.shibboleth.oidc.profile.messaging.context.OIDCPeerEntityContext;
import net.shibboleth.shared.logic.Constraint;
import net.shibboleth.shared.primitive.LoggerFactory;
-import net.shibboleth.sp.profile.AbstractAgentAction;
/**
* Abstract class for actions based on the {@link AuthenticationResponse} located under
@@ -42,7 +42,7 @@ import net.shibboleth.sp.profile.AbstractAgentAction;
*
* <p>Also makes available the OpenID Provider metadata context.</p>
*/
-public abstract class AbstractAuthorizationResponseAction extends AbstractAgentAction {
+public abstract class AbstractAuthorizationResponseAction extends AbstractProfileAction {
/** Class logger. */
@Nonnull private final Logger log = LoggerFactory.getLogger(AbstractAuthorizationResponseAction.class);
diff --git a/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/AbstractHttpOAuthAction.java b/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/AbstractHttpOAuthAction.java
index a710590..eecb03c 100644
--- a/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/AbstractHttpOAuthAction.java
+++ b/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/AbstractHttpOAuthAction.java
@@ -25,6 +25,7 @@ import org.apache.hc.client5.http.classic.methods.HttpUriRequest;
import org.apache.hc.client5.http.protocol.HttpClientContext;
import org.apache.hc.core5.http.ClassicHttpRequest;
import org.apache.hc.core5.http.io.HttpClientResponseHandler;
+import org.opensaml.messaging.handler.MessageHandlerException;
import org.opensaml.profile.context.ProfileRequestContext;
import org.opensaml.security.httpclient.HttpClientSecurityParameters;
import org.opensaml.security.httpclient.HttpClientSecuritySupport;
@@ -40,7 +41,6 @@ import net.shibboleth.shared.annotation.constraint.ThreadSafeAfterInit;
import net.shibboleth.shared.component.ComponentInitializationException;
import net.shibboleth.shared.logic.Constraint;
import net.shibboleth.shared.primitive.LoggerFactory;
-import net.shibboleth.sp.oidc.exception.OIDCRPException;
/**
* An abstract class for OIDC/OAuth actions that make synchronous HTTP requests and return types of
@@ -159,25 +159,25 @@ public abstract class AbstractHttpOAuthAction<T extends Response>
*
* @return a successful decoded response. Never {@code null}.
*
- * @throws OIDCRPException on error making the request, or if an error response is returned.
+ * @throws MessageHandlerException on error making the request, or if an error response is returned.
*/
@Nonnull protected T handleRequest(@Nonnull final ProfileRequestContext profileRequestContext,
- @Nullable final AbstractAuthenticatableOIDCContext authenticatableContext) throws OIDCRPException {
+ @Nullable final AbstractAuthenticatableOIDCContext authenticatableContext) throws MessageHandlerException {
try {
final ClassicHttpRequest request = getHttpRequestEncoderStrategy().apply(profileRequestContext);
if (request == null) {
- throw new OIDCRPException("Unable to encode HTTP request");
+ throw new MessageHandlerException("Unable to encode HTTP request");
}
final T response = executeHttpRequest(request, authenticatableContext);
if (response == null) {
- throw new OIDCRPException("Unable to process HTTP response");
+ throw new MessageHandlerException("Unable to process HTTP response");
} else if (!response.indicatesSuccess()) {
- throw new OIDCRPException(formatErrorResponse(((ErrorResponse)response).getErrorObject()));
+ throw new MessageHandlerException(formatErrorResponse(((ErrorResponse)response).getErrorObject()));
}
return response;
} catch (final IOException e) {
log.error("{} Unable to perform HTTP request and return response",getLogPrefix(),e);
- throw new OIDCRPException(e);
+ throw new MessageHandlerException(e);
}
}
diff --git a/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/DefaultClaimMergingStrategy.java b/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/DefaultClaimMergingStrategy.java
new file mode 100644
index 0000000..6e6d1b7
--- /dev/null
+++ b/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/DefaultClaimMergingStrategy.java
@@ -0,0 +1,83 @@
+/*
+ * Licensed 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.sp.oidc.profile.impl;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.function.BinaryOperator;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.slf4j.Logger;
+
+import com.nimbusds.openid.connect.sdk.claims.ClaimsSet;
+
+import net.shibboleth.shared.primitive.LoggerFactory;
+
+/**
+ * A default merging strategy for combing claims in the UserInfo response with those from the id_token.
+ * <ol>
+ * <li>If one of UserInfo or id_token claims are null, the other is returned.</li>
+ * <li>If both input claims are null, an empty claims set is returned.</li>
+ * <li>Merges the id_token claims into the UserInfo claims, the value of a claim from the id_token
+ * is taken over that from the UserInfo response if the claim keys clash.</li>
+ * </ol>
+ */
+public class DefaultClaimMergingStrategy implements BinaryOperator<ClaimsSet> {
+
+ /** Class logger.*/
+ @Nonnull private final Logger log = LoggerFactory.getLogger(DefaultClaimMergingStrategy.class);
+
+ @Override
+ @Nonnull public ClaimsSet apply(@Nullable final ClaimsSet userInfo, @Nullable final ClaimsSet idToken) {
+
+ if (userInfo == null && idToken != null) {
+ final ClaimsSet singleSet = new ClaimsSet();
+ singleSet.putAll(idToken.toJSONObject());
+ return singleSet;
+ }
+ if (userInfo != null && idToken == null) {
+ final ClaimsSet singleSet = new ClaimsSet();
+ singleSet.putAll(userInfo.toJSONObject());
+ return singleSet;
+ }
+ // Now catch if either is null
+ if (userInfo == null || idToken == null) {
+ // return empty claimsset
+ return new ClaimsSet();
+ }
+
+ final Map<String, Object> idTokenAsMap = idToken.toJSONObject();
+ // Treat JSONObject as the base map representation.
+ final Map<String, Object> userInfoClaimsAsMap = userInfo.toJSONObject();
+
+ // Add UserInfo claims as a base
+ final Map<String, Object> mergedClaimsMap = new HashMap<>(userInfoClaimsAsMap);
+
+ // Merge id_token claims into userInfo claims, take id_token claim if conflict
+ idTokenAsMap.forEach((key, value) ->
+ mergedClaimsMap.merge(key, value, (v1, v2)-> {
+ log.trace("Claim '{}' exists in id_token and UserInfo response, taking id_token value '{}'",key, v2);
+ return v2;
+ }));
+
+
+ final ClaimsSet mergedClaimsSet = new ClaimsSet();
+ mergedClaimsSet.putAll(mergedClaimsMap);
+ return mergedClaimsSet;
+ }
+
+}
diff --git a/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/DefaultClaimSanitizationStrategy.java b/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/DefaultClaimSanitizationStrategy.java
new file mode 100644
index 0000000..3991bfb
--- /dev/null
+++ b/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/DefaultClaimSanitizationStrategy.java
@@ -0,0 +1,72 @@
+/*
+ * Licensed 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.sp.oidc.profile.impl;
+
+import java.util.Map;
+import java.util.Set;
+import java.util.function.UnaryOperator;
+import java.util.stream.Collectors;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import com.nimbusds.openid.connect.sdk.claims.ClaimsSet;
+
+import net.shibboleth.oidc.security.jwt.claims.impl.IDTokenClaims;
+import net.shibboleth.oidc.security.jwt.claims.impl.JWTClaims;
+
+/**
+ * Produce a claims set from the JWT claims set without either the validation claims or claims with null values.
+ * Leaving the identity, authorization, and misc. claims.
+ *
+ */
+public class DefaultClaimSanitizationStrategy implements UnaryOperator<ClaimsSet> {
+
+ /** The set of validation claims to filter out of the input claims.*/
+ @Nonnull private final Set<String> validationClaims;
+
+ /** Constructor.*/
+ public DefaultClaimSanitizationStrategy() {
+ final Set<String> validationClaimsBuilt = Set.of(IDTokenClaims.AUTHORIZED_PARTY.getClaimName(),
+ IDTokenClaims.NONCE.getClaimName(),
+ IDTokenClaims.AUTHENTICATION_TIME.getClaimName(),
+ IDTokenClaims.AUTHENTICATION_CONTEXT_CLASS_REFERENCE.getClaimName(),
+ IDTokenClaims.AUTHENTICATION_METHODS_REFERENCES.getClaimName(),
+ JWTClaims.ISSUER_CLAIM.getClaimName(),
+ JWTClaims.ISSUED_AT_CLAIM.getClaimName(),
+ JWTClaims.AUDIENCE_CLAIM.getClaimName(),
+ JWTClaims.EXPIRATION_TIME_CLAIM.getClaimName(),
+ "at_hash");
+ assert validationClaimsBuilt != null;
+ validationClaims = validationClaimsBuilt;
+ }
+
+ @Override
+ public ClaimsSet apply(@Nullable final ClaimsSet jwtClaims) {
+ if (jwtClaims == null) {
+ return new ClaimsSet();
+ }
+ final ClaimsSet sanitizedClaims = new ClaimsSet();
+ final Map<String, Object> filteredMap = jwtClaims.toJSONObject().entrySet()
+ .stream()
+ .filter(c -> c.getValue() != null)
+ .filter(c -> !validationClaims.contains(c.getKey()))
+ .filter(c -> c.getKey() != null)
+ .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
+ sanitizedClaims.putAll(filteredMap);
+ return sanitizedClaims;
+ }
+
+}
diff --git a/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/ExchangeCodeForAccessToken.java b/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/ExchangeCodeForAccessToken.java
index c372a70..c353322 100644
--- a/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/ExchangeCodeForAccessToken.java
+++ b/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/ExchangeCodeForAccessToken.java
@@ -20,6 +20,7 @@ import javax.annotation.Nonnull;
import org.opensaml.messaging.context.MessageContext;
import org.opensaml.messaging.context.navigate.ChildContextLookup;
+import org.opensaml.messaging.handler.MessageHandlerException;
import org.opensaml.profile.action.ActionSupport;
import org.opensaml.profile.action.EventIds;
import org.opensaml.profile.context.ProfileRequestContext;
@@ -35,7 +36,6 @@ import net.shibboleth.oidc.profile.context.AccessTokenResponseContext;
import net.shibboleth.oidc.profile.messaging.context.OIDCPeerEntityContext;
import net.shibboleth.shared.logic.Constraint;
import net.shibboleth.shared.primitive.LoggerFactory;
-import net.shibboleth.sp.oidc.exception.OIDCRPException;
/**
@@ -108,10 +108,10 @@ public class ExchangeCodeForAccessToken extends AbstractHttpOAuthAction<TokenRes
log.trace("{} Token request response was successful '{}'",getLogPrefix(),
tokenResponse.indicatesSuccess());
} else {
- throw new OIDCRPException("Token response was not of the expected format, expected OIDC token "
+ throw new MessageHandlerException("Token response was not of the expected format, expected OIDC token "
+ "response, got " + responseObject.getClass().getSimpleName());
}
- } catch (final OIDCRPException e) {
+ } catch (final MessageHandlerException e) {
log.error("{} Failed to exchange authorisation code for token result",getLogPrefix(), e);
ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.AUTHN_EXCEPTION);
}
diff --git a/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/ProcessEndUserClaims.java b/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/ProcessEndUserClaims.java
new file mode 100644
index 0000000..859c88d
--- /dev/null
+++ b/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/ProcessEndUserClaims.java
@@ -0,0 +1,293 @@
+/*
+ * Licensed 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.sp.oidc.profile.impl;
+
+import java.text.ParseException;
+import java.util.function.BinaryOperator;
+import java.util.function.Function;
+import java.util.function.UnaryOperator;
+
+import javax.annotation.Nonnull;
+
+import org.opensaml.messaging.context.navigate.ChildContextLookup;
+import org.opensaml.profile.action.ActionSupport;
+import org.opensaml.profile.action.EventIds;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.opensaml.profile.context.navigate.InboundMessageContextLookup;
+import org.slf4j.Logger;
+
+import com.nimbusds.common.contenttype.ContentType;
+import com.nimbusds.jwt.JWTClaimsSet;
+import com.nimbusds.openid.connect.sdk.OIDCTokenResponse;
+import com.nimbusds.openid.connect.sdk.UserInfoSuccessResponse;
+import com.nimbusds.openid.connect.sdk.claims.ClaimsSet;
+
+import net.shibboleth.idp.profile.AbstractProfileAction;
+import net.shibboleth.oidc.profile.context.AccessTokenResponseContext;
+import net.shibboleth.oidc.profile.context.EndUserClaimsContext;
+import net.shibboleth.oidc.profile.context.UserInfoResponseContext;
+import net.shibboleth.shared.annotation.constraint.NonnullBeforeExec;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.primitive.LoggerFactory;
+
+/**
+ * Process the end-user claims from the id_token and possible UserInfo claims sets.
+ *
+ * <p>Sanitized both claims sets using a replaceable strategy. For example,
+ * by default to remove standard JWT 'validation' claims that should not be exposed further by the system.</p>
+ *
+ * <p>Merge the claims sets together to produce an aggregate claims set. The UserInfo claims can
+ * be empty i.e. claims from the UserInfo endpoint were not requested.</p>
+ *
+ * @event {@link org.opensaml.profile.action.EventIds#PROCEED_EVENT_ID}
+ * @event {@link EventIds#INVALID_PROFILE_CTX}
+ * @post Add a set of merged and sanatized claims to the {@link EndUserClaimsContext}.
+ */
+public class ProcessEndUserClaims extends AbstractProfileAction {
+
+ /** Class logger.*/
+ @Nonnull private final Logger log = LoggerFactory.getLogger(ProcessEndUserClaims.class);
+
+ /** Strategy used to look up the {@link UserInfoResponseContext}. */
+ @Nonnull private Function<ProfileRequestContext, UserInfoResponseContext>
+ userInfoResponseContextLookupStrategy;
+
+ /** Strategy used to look up the {@link AccessTokenResponseContext} . */
+ @Nonnull private Function<ProfileRequestContext, AccessTokenResponseContext>
+ accessTokenResponseContextLookupStrategy;
+
+ /** Strategy used to look up the {@link EndUserClaimsContext} to set the parameters for. */
+ @Nonnull private Function<ProfileRequestContext, EndUserClaimsContext>
+ endUserClaimsContextLookupStrategy;
+
+ /** The strategy used to merge UserInfo claims with id_token claims.*/
+ @Nonnull private BinaryOperator<ClaimsSet> claimMergingStrategy;
+
+ /**
+ * The strategy used to sanitize claims in an input claimset. By default, produces a set of
+ * claims without the validation claims (e.g. nonce, exp), but leaving the identity, authorization and
+ * misc claims.
+ */
+ @Nonnull private UnaryOperator<ClaimsSet> claimSanitizationStrategy;
+
+ /** The stashed UserInfo claims. This is allowably {@literal empty} if the UserInfo endpoint was not used.*/
+ @NonnullBeforeExec private ClaimsSet userInfoClaims;
+
+ /**
+ * The stashed id_token claims. This should never be {@literal null} or {@literal empty} once
+ * {@link #doPreExecute(ProfileRequestContext)} has run.
+ */
+ @NonnullBeforeExec private JWTClaimsSet idTokenClaims;
+
+
+ /** Constructor.*/
+ public ProcessEndUserClaims() {
+ userInfoResponseContextLookupStrategy =
+ new ChildContextLookup<>(UserInfoResponseContext.class).compose(
+ new InboundMessageContextLookup());
+
+ accessTokenResponseContextLookupStrategy =
+ new ChildContextLookup<>(AccessTokenResponseContext.class).compose(
+ new InboundMessageContextLookup());
+
+ // Will create context.
+ endUserClaimsContextLookupStrategy =
+ new ChildContextLookup<>(EndUserClaimsContext.class, true).compose(
+ new InboundMessageContextLookup());
+
+ claimMergingStrategy = new DefaultClaimMergingStrategy();
+ claimSanitizationStrategy = new DefaultClaimSanitizationStrategy();
+ }
+
+
+ /**
+ * Set the strategy used to merge UserInfo claims with id_token claims.
+ *
+ * @param strategy the strategy to use.
+ */
+ public void setClaimMergingStrategy(@Nonnull final BinaryOperator<ClaimsSet> strategy) {
+ checkSetterPreconditions();
+
+ claimMergingStrategy = Constraint.isNotNull(strategy,
+ "ClaimMergingStrategy cannot be null");
+ }
+
+ /**
+ * Set the strategy used to sanitize claims from both the id_token and UserInfo
+ * claims set to produce a clean claims set.
+ *
+ * @param strategy the strategy to use.
+ */
+ public void setClaimSanitizationStrategy(@Nonnull final UnaryOperator<ClaimsSet> strategy) {
+ checkSetterPreconditions();
+
+ claimSanitizationStrategy = Constraint.isNotNull(strategy,
+ "ClaimSanatizationStrategy cannot be null");
+ }
+
+ /**
+ * Set whether to enable claim sanitization. If true, whatever claimSanatizationStrategy
+ * is set is used. If false, a no-op strategy is created which just returns a new claims set
+ * based on the same claims that exist in the input claims set. By default, claims sanitization
+ * uses the {@link DefaultClaimSanitizationStrategy}.
+ *
+ * @param enable enable or disable claims sanitization
+ */
+ public void setEnableClaimSanitizationStrategy(final boolean enable) {
+ if (!enable) {
+ claimSanitizationStrategy = c -> {
+ final ClaimsSet claims = new ClaimsSet();
+ claims.putAll(c);
+ return claims;
+ };
+ }
+ }
+
+ /**
+ * Set the strategy used to lookup a {@link EndUserClaimsContext}.
+ *
+ * @param strategy the strategy
+ */
+ public void setEndUserClaimsContextLookupStrategy(
+ @Nonnull final Function<ProfileRequestContext, EndUserClaimsContext> strategy) {
+ checkSetterPreconditions();
+
+ endUserClaimsContextLookupStrategy = Constraint.isNotNull(strategy,
+ "EndUserClaimsContextLookupStrategy cannot be null");
+ }
+
+
+ /**
+ * Set the strategy used to look up a {@link AccessTokenResponseContext}.
+ *
+ * @param strategy lookup strategy
+ */
+ public void setAccessTokenResponseContextLookupStrategy(
+ @Nonnull final Function<ProfileRequestContext, AccessTokenResponseContext> strategy) {
+ checkSetterPreconditions();
+
+ accessTokenResponseContextLookupStrategy = Constraint.isNotNull(strategy,
+ "TokenResponseContext lookup strategy cannot be null");
+ }
+
+ /**
+ * Set the strategy used to look up a {@link UserInfoResponseContext}.
+ *
+ * @param strategy lookup strategy
+ */
+ public void setUserInfoResponseContextLookupStrategy(
+ @Nonnull final Function<ProfileRequestContext, UserInfoResponseContext> strategy) {
+ checkSetterPreconditions();
+
+ userInfoResponseContextLookupStrategy = Constraint.isNotNull(strategy,
+ "UserInfoResponseContext lookup strategy cannot be null");
+ }
+
+ // Checkstyle: CyclomaticComplexity OFF
+ @Override protected boolean doPreExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
+
+ if (!super.doPreExecute(profileRequestContext)) {
+ return false;
+ }
+
+ final UserInfoResponseContext userInfoCtx =
+ userInfoResponseContextLookupStrategy.apply(profileRequestContext);
+ final UserInfoSuccessResponse userInfo = userInfoCtx != null ? userInfoCtx.getUserInfo() : null;
+
+ if (userInfoCtx == null || userInfo == null) {
+ log.trace("{} No UserInfo response context returned by lookup strategy, creating empty "
+ + "UserInfo claims", getLogPrefix());
+ userInfoClaims = new ClaimsSet();
+ } else if (userInfo.getEntityContentType() == ContentType.APPLICATION_JSON) {
+ userInfoClaims = userInfo.getUserInfo();
+ } else if (userInfo.getEntityContentType() == ContentType.APPLICATION_JWT) {
+ try {
+ final ClaimsSet claims = new ClaimsSet();
+ claims.putAll(userInfo.getUserInfoJWT().getJWTClaimsSet().getClaims());
+ userInfoClaims = claims;
+ } catch (final ParseException e) {
+ log.warn("Unable to extract UserInfo claims from JWT claimsset", e);
+ return false;
+ }
+ } else {
+ log.warn("Unable to extract UserInfo claims, unknown entity content type");
+ return false;
+ }
+
+ final AccessTokenResponseContext tokenResponseCtx =
+ accessTokenResponseContextLookupStrategy.apply(profileRequestContext);
+ final OIDCTokenResponse tokenResponse = tokenResponseCtx != null ? tokenResponseCtx.getTokenResponse() : null;
+
+ if (tokenResponseCtx == null || tokenResponse == null) {
+ log.debug("{} No AccessTokenResponseContext or Access Token returned by lookup strategy", getLogPrefix());
+ ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
+ return false;
+ }
+ if (tokenResponse.getOIDCTokens() == null ||
+ tokenResponse.getOIDCTokens().getIDToken() == null) {
+ log.debug("{} AccessTokenResponseContext did not contain an id_token", getLogPrefix());
+ ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
+ return false;
+ }
+ try {
+ idTokenClaims = tokenResponse.getOIDCTokens().getIDToken().getJWTClaimsSet();
+ if (idTokenClaims == null) {
+ log.debug("{} AccessTokenResponseContext did not contain an id_token with accessible claims, "
+ + "possibly still encrypted",
+ getLogPrefix());
+ ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
+ return false;
+ }
+ } catch (final ParseException e) {
+ log.debug("{} Unable to parse claims from id_token", getLogPrefix());
+ ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
+ return false;
+ }
+
+ return true;
+ }
+
+ // Checkstyle: CyclomaticComplexity ON
+
+ @Override protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
+
+ if (log.isTraceEnabled()) {
+ log.trace("{} Processing {} claims", getLogPrefix(),
+ userInfoClaims.toJSONObject().size() > 0 ? "UserInfo and ID Token" : "ID Token");
+ }
+
+ final ClaimsSet idToken = new ClaimsSet();
+ idToken.putAll(idTokenClaims.toJSONObject());
+
+ final ClaimsSet sanitizedUserInfoClaims = claimSanitizationStrategy.apply(userInfoClaims);
+ final ClaimsSet sanitizedIdTokenClaims = claimSanitizationStrategy.apply(idToken);
+
+ final ClaimsSet mergedClaims = claimMergingStrategy.apply(sanitizedUserInfoClaims, sanitizedIdTokenClaims);
+
+ // Add to end user claims context both the merged claims, and the parsed id_token claims.
+ // The id_token claims are stashed here to avoid re-parsing downstream.
+ final var idTokenClaimsCopy = idTokenClaims;
+ assert idTokenClaimsCopy != null;
+ endUserClaimsContextLookupStrategy.apply(profileRequestContext)
+ .setEndUserClaims(mergedClaims != null ? mergedClaims : new ClaimsSet())
+ .setUnprocessedIdTokenClaims(idTokenClaimsCopy);
+
+ if (log.isTraceEnabled() && mergedClaims != null) {
+ log.trace("{} Merged and sanitized claims to produce the claims set '{}'",
+ getLogPrefix(), mergedClaims.toJSONString());
+ }
+ }
+
+}
diff --git a/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/UserInfoEndpointLookup.java b/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/UserInfoEndpointLookup.java
new file mode 100644
index 0000000..f972669
--- /dev/null
+++ b/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/UserInfoEndpointLookup.java
@@ -0,0 +1,107 @@
+/*
+ * Licensed 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.sp.oidc.profile.impl;
+
+import java.util.function.Function;
+
+import javax.annotation.Nonnull;
+
+import org.opensaml.messaging.context.MessageContext;
+import org.opensaml.messaging.context.navigate.ChildContextLookup;
+import org.opensaml.messaging.handler.MessageHandlerException;
+import org.opensaml.profile.action.ActionSupport;
+import org.opensaml.profile.action.EventIds;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.opensaml.profile.context.navigate.InboundMessageContextLookup;
+import org.slf4j.Logger;
+
+import com.nimbusds.openid.connect.sdk.UserInfoResponse;
+
+import net.shibboleth.idp.authn.AuthnEventIds;
+import net.shibboleth.oidc.profile.context.AccessTokenResponseContext;
+import net.shibboleth.oidc.profile.context.UserInfoResponseContext;
+import net.shibboleth.oidc.profile.core.OidcEventIds;
+import net.shibboleth.oidc.profile.messaging.context.OIDCPeerEntityContext;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.primitive.LoggerFactory;
+
+/**
+ * Request information from the UserInfo OAuth2.0 endpoint using the access_token already present
+ * in the context. Return consented claims about the subject.
+ *
+ * @event {@link org.opensaml.profile.action.EventIds#PROCEED_EVENT_ID}
+ * @event {@link EventIds#INVALID_PROFILE_CTX}
+ * @event {@link AuthnEventIds#AUTHN_EXCEPTION}
+ * @post Add a claims returned from the UserInfo endpoint to the {@link UserInfoResponseContext}.
+ */
+public class UserInfoEndpointLookup extends AbstractHttpOAuthAction<UserInfoResponse> {
+
+ /** Class logger.*/
+ @Nonnull private final Logger log = LoggerFactory.getLogger(UserInfoEndpointLookup.class);
+
+ /** Strategy used to look up the {@link AccessTokenResponseContext} to set the parameters for. */
+ @Nonnull private Function<ProfileRequestContext, UserInfoResponseContext>
+ userInfoResponseContextLookupStrategy;
+
+
+ /** Constructor.*/
+ public UserInfoEndpointLookup() {
+ userInfoResponseContextLookupStrategy =
+ new ChildContextLookup<>(UserInfoResponseContext.class, true).compose(
+ new InboundMessageContextLookup());
+ }
+
+ /**
+ * Set the strategy used to look up a {@link UserInfoResponseContext}.
+ *
+ * @param strategy lookup strategy
+ */
+ public void setUserInfoResponseContextLookupStrategy(
+ @Nonnull final Function<ProfileRequestContext, UserInfoResponseContext> strategy) {
+ checkSetterPreconditions();
+
+ userInfoResponseContextLookupStrategy = Constraint.isNotNull(strategy,
+ "UserInfoResponseContext lookup strategy cannot be null");
+ }
+
+ @Override protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
+
+ final MessageContext inboundMessageContext = profileRequestContext.getInboundMessageContext();
+ final String provider = inboundMessageContext != null ? inboundMessageContext
+ .ensureSubcontext(OIDCPeerEntityContext.class).getIdentifier() : "na";
+
+ log.debug("{} Requesting claims from UserInfo endpoint from provider '{}'", getLogPrefix(),
+ provider);
+
+ final UserInfoResponseContext userInfoCtx =
+ userInfoResponseContextLookupStrategy.apply(profileRequestContext);
+ if (userInfoCtx == null) {
+ log.debug("{} No UserInfo response context returned by lookup strategy", getLogPrefix());
+ ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
+ return;
+ }
+
+ try {
+ final UserInfoResponse response = handleRequest(profileRequestContext, userInfoCtx);
+ if (response.indicatesSuccess()) {
+ userInfoCtx.setUserInfo(response.toSuccessResponse());
+ }
+ } catch (final MessageHandlerException e) {
+ log.error("{} Unable to return claims from UserInfo endpoint",getLogPrefix(),e);
+ ActionSupport.buildEvent(profileRequestContext, OidcEventIds.INVALID_USERINFO_CLAIMS);
+ }
+ }
+
+}
diff --git a/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/ValidateTokenClaims.java b/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/ValidateTokenClaims.java
index 0583054..24486f2 100644
--- a/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/ValidateTokenClaims.java
+++ b/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/ValidateTokenClaims.java
@@ -39,12 +39,10 @@ import net.shibboleth.shared.annotation.constraint.NonnullBeforeExec;
import net.shibboleth.shared.component.ComponentInitializationException;
import net.shibboleth.shared.logic.Constraint;
import net.shibboleth.shared.primitive.LoggerFactory;
-import net.shibboleth.sp.oidc.exception.OIDCRPException;
/**
* Action that validates the claims of a JWT using the supplied
- * {@link ClaimsValidator claims validator}. The verifier <b>must</b> be thread-safe and validate, at
- * minimum, the claims set against the OpenID Connect core 1.0 section 3.1.3.7 specification.
+ * {@link ClaimsValidator claims validator}. The verifier <b>must</b> be thread-safe and validate.
*
*
* @pre <pre>ProfileRequestContext.getSubcontext(AuthenticationContext.class, false) != null</pre>
@@ -135,9 +133,9 @@ public class ValidateTokenClaims extends AbstractProfileAction {
//parse the claimset here, so parsing only has to happen once, and we fail fast on error (e.g. bad JSON)
claimsSet = token.getJWTClaimsSet();
if (claimsSet == null) {
- throw new OIDCRPException("JWT ClaimsSet is null");
+ throw new JWTValidationException("JWT ClaimsSet is null");
}
- } catch (final ParseException | OIDCRPException e) {
+ } catch (final ParseException | JWTValidationException e) {
log.error("{} JWT Claimset is not available", getLogPrefix(),e);
ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.INVALID_AUTHN_CTX);
return false;
diff --git a/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/ValidateUserInfoJSONObjectClaims.java b/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/ValidateUserInfoJSONObjectClaims.java
new file mode 100644
index 0000000..627b95c
--- /dev/null
+++ b/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/ValidateUserInfoJSONObjectClaims.java
@@ -0,0 +1,221 @@
+/*
+ * Licensed 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.sp.oidc.profile.impl;
+
+import java.text.ParseException;
+import java.util.function.Function;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.messaging.context.navigate.ChildContextLookup;
+import org.opensaml.profile.action.ActionSupport;
+import org.opensaml.profile.action.EventIds;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.opensaml.profile.context.navigate.InboundMessageContextLookup;
+import org.slf4j.Logger;
+
+import com.nimbusds.common.contenttype.ContentType;
+import com.nimbusds.jwt.JWTClaimsSet;
+import com.nimbusds.openid.connect.sdk.OIDCTokenResponse;
+import com.nimbusds.openid.connect.sdk.UserInfoResponse;
+import com.nimbusds.openid.connect.sdk.UserInfoSuccessResponse;
+import com.nimbusds.openid.connect.sdk.claims.ClaimsSet;
+
+import net.shibboleth.idp.profile.AbstractProfileAction;
+import net.shibboleth.oidc.profile.context.AccessTokenResponseContext;
+import net.shibboleth.oidc.profile.context.UserInfoResponseContext;
+import net.shibboleth.oidc.profile.core.OidcEventIds;
+import net.shibboleth.shared.annotation.constraint.NonnullBeforeExec;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.primitive.LoggerFactory;
+
+/**
+ * Validate a successful UserInfo JSON Object Response according to section 5.3.2 of OpenID Connect Core 1.0.
+ *
+ * @pre <pre>ProfileRequestContext.getSubcontext(AuthenticationContext.class, false) != null</pre>
+ * @event {@link org.opensaml.profile.action.EventIds#PROCEED_EVENT_ID}
+ * @event {@link OidcEventIds#INVALID_USERINFO_CLAIMS}
+ * @event {@link EventIds#INVALID_PROFILE_CTX}
+ */
+public class ValidateUserInfoJSONObjectClaims extends AbstractProfileAction {
+
+ /** Class logger.*/
+ @Nonnull private final Logger log = LoggerFactory.getLogger(ValidateUserInfoJSONObjectClaims.class);
+
+ /** Strategy used to look up the {@link UserInfoResponseContext}. */
+ @Nonnull private Function<ProfileRequestContext, UserInfoResponseContext> userInfoResponseContextLookupStrategy;
+
+ /** Strategy used to look up the {@link AccessTokenResponseContext}. */
+ @Nonnull private Function<ProfileRequestContext, AccessTokenResponseContext> tokenResponseContextLookupStrategy;
+
+ /** The stashed user info response context.*/
+ @NonnullBeforeExec private UserInfoResponseContext userInfoCtx;
+
+ /** The stashed user info response.*/
+ @NonnullBeforeExec private UserInfoSuccessResponse userInfo;
+
+ /** The stashed id_token claims.*/
+ @NonnullBeforeExec private JWTClaimsSet idTokenClaims;
+
+ /** Constructor.*/
+ public ValidateUserInfoJSONObjectClaims() {
+ userInfoResponseContextLookupStrategy =
+ new ChildContextLookup<>(UserInfoResponseContext.class).compose(
+ new InboundMessageContextLookup());
+ tokenResponseContextLookupStrategy =
+ new ChildContextLookup<>(AccessTokenResponseContext.class).compose(
+ new InboundMessageContextLookup());
+ }
+
+ /**
+ * Set the strategy used to look up a {@link AccessTokenResponseContext}.
+ *
+ * @param strategy lookup strategy
+ */
+ public void setTokenResponseContextLookupStrategy(
+ @Nonnull final Function<ProfileRequestContext, AccessTokenResponseContext> strategy) {
+ checkSetterPreconditions();
+
+ tokenResponseContextLookupStrategy = Constraint.isNotNull(strategy,
+ "TokenResponseContext lookup strategy cannot be null");
+ }
+
+ /**
+ * Set the strategy used to look up a {@link UserInfoResponseContext}.
+ *
+ * @param strategy lookup strategy
+ */
+ public void setUserInfoResponseContextLookupStrategy(
+ @Nonnull final Function<ProfileRequestContext, UserInfoResponseContext> strategy) {
+ checkSetterPreconditions();
+
+ userInfoResponseContextLookupStrategy = Constraint.isNotNull(strategy,
+ "UserInfoResponseContext lookup strategy cannot be null");
+ }
+
+ @Override protected boolean doPreExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
+
+ if (!super.doPreExecute(profileRequestContext)) {
+ return false;
+ }
+
+ userInfoCtx =
+ userInfoResponseContextLookupStrategy.apply(profileRequestContext);
+ userInfo = userInfoCtx != null ? userInfoCtx.getUserInfo() : null;
+ if (userInfo == null) {
+ log.debug("{} No UserInfo response returned by lookup strategy", getLogPrefix());
+ ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
+ return false;
+ }
+
+ final AccessTokenResponseContext tokenResponseCtx =
+ tokenResponseContextLookupStrategy.apply(profileRequestContext);
+ if (tokenResponseCtx == null) {
+ log.debug("{} No AccessTokenResponseContext returned by lookup strategy", getLogPrefix());
+ ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
+ return false;
+ }
+ final OIDCTokenResponse tokenResponse = tokenResponseCtx.getTokenResponse();
+ if (tokenResponse == null || tokenResponse.getOIDCTokens().getIDToken() == null) {
+ log.debug("{} AccessTokenResponseContext did not contain an id_token", getLogPrefix());
+ ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
+ return false;
+ }
+ try {
+ idTokenClaims = tokenResponse.getOIDCTokens().getIDToken().getJWTClaimsSet();
+ if (idTokenClaims == null) {
+ log.debug("{} AccessTokenResponseContext did not contain an id_token with accessible claims, "
+ + "possibly still encrypted",
+ getLogPrefix());
+ ActionSupport.buildEvent(profileRequestContext, OidcEventIds.INVALID_ID_TOKEN);
+ return false;
+ }
+ } catch (final ParseException e) {
+ log.debug("{} Unable to parse claims from id_token", getLogPrefix());
+ ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
+ return false;
+ }
+
+ return true;
+ }
+
+ @Override protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
+
+ log.trace("{} Validating UserInfo JSON Object claims", getLogPrefix());
+
+ assert userInfo != null;
+ final ClaimsSet claims = resolveClaimsSet(userInfo);
+ if (claims == null) {
+ log.debug("{} UserInfo claims can not be resolved", getLogPrefix());
+ ActionSupport.buildEvent(profileRequestContext, OidcEventIds.INVALID_USERINFO_CLAIMS);
+ return;
+ }
+ final String subFromUserInfo = claims.getStringClaim("sub");
+ if (subFromUserInfo == null) {
+ log.debug("{} UserInfo claims does not contain the 'sub' claim, it must", getLogPrefix());
+ ActionSupport.buildEvent(profileRequestContext, OidcEventIds.INVALID_USERINFO_CLAIMS);
+ return;
+ }
+ // sub must match to id_token sub
+ if (!idTokenClaims.getSubject().equals(subFromUserInfo)){
+ log.error("{} UserInfo claims about subject '{}' but id_token about subject '{}', mismatch",
+ getLogPrefix(), subFromUserInfo, idTokenClaims.getSubject());
+ ActionSupport.buildEvent(profileRequestContext, OidcEventIds.INVALID_USERINFO_CLAIMS);
+ return;
+ }
+ log.debug("{} UserInfo claims are valid for '{}'", getLogPrefix(), claims.getStringClaim("sub"));
+
+ }
+
+ /**
+ * Resolve the UserInfo claims from the user info response. If JWT type, extract from the JWT claims. If plain JSON
+ * object type, resolve directly from the UserInfo claims set.
+ *
+ * @param response the UserInfo response
+ *
+ * @return the UserInfo claims. Return {@code null} if not found, wrong response type, or there was a parsing
+ * exception
+ */
+ @Nullable private ClaimsSet resolveClaimsSet(@Nonnull final UserInfoResponse response) {
+ if (!response.indicatesSuccess()) {
+ return null;
+ }
+ final UserInfoSuccessResponse successResponse = response.toSuccessResponse();
+ if (successResponse.getEntityContentType() == ContentType.APPLICATION_JWT) {
+ try {
+ final JWTClaimsSet claims = successResponse.getUserInfoJWT().getJWTClaimsSet();
+ if (claims == null) {
+ log.debug("{} UserInfo claims are not available, check response is not still encrypted",
+ getLogPrefix());
+ return null;
+ }
+ final ClaimsSet claimsConverted = new ClaimsSet();
+ claimsConverted.putAll(claims.toJSONObject());
+ return claimsConverted;
+ } catch (final ParseException e) {
+ log.warn("{} UserInfo claims could not be extracted from the JWT claims set", getLogPrefix(), e);
+ return null;
+ }
+
+ } else if (successResponse.getEntityContentType() == ContentType.APPLICATION_JSON) {
+ return successResponse.getUserInfo();
+ } else {
+ log.debug("{} UserInfo claims are not available, unknown entity content type", getLogPrefix());
+ return null;
+ }
+ }
+
+}
--
To stop receiving notification emails like this one, please contact
the administrator of this repository.
More information about the commits
mailing list