[java-plugin-shibd-oidc] branch main updated: JSHIBDOIDC-8 - Add back missing token validation logic
Codeberg
noreply at shibboleth.net
Fri Jan 16 14:55:39 UTC 2026
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/6dc197b91fef92898d12c128430454cd5cd340d4
The following commit(s) were added to refs/heads/main by this push:
new 6dc197b JSHIBDOIDC-8 - Add back missing token validation logic
6dc197b is described below
commit 6dc197b91fef92898d12c128430454cd5cd340d4
Author: Phil Smart <philip.smart at jisc.ac.uk>
AuthorDate: Fri Jan 16 14:55:29 2026 +0000
JSHIBDOIDC-8 - Add back missing token validation logic
- Add AudienceClaimsValidator
- Add NonceClaimValidator
- Add AuthenticationTimeClaimValidator
- Change to new automatic flow detection
- Add more state to be stored between authentication request and
authentication response.
- Improve tests
https://shibboleth.atlassian.net/browse/JSHIBDOIDC-8
---
pom.xml | 8 +
sp-oidc-api/pom.xml | 6 +
.../oidc/context/AuthnRequestStateDataContext.java | 50 +++
...nticationRequestFromOutboundLookupStrategy.java | 43 +++
.../navigate/ClientIdBiFunctionLookupStrategy.java | 55 ++++
.../profile/AuthenticationRequestStateData.java | 303 ++++++++++++++++++
.../AuthenticationRequestStateDataTest.java | 134 ++++++++
.../META-INF/net.shibboleth.idp/postconfig.xml | 2 -
.../sp/consumer/oidc/code/post/post-beans.xml | 22 --
.../sp/consumer/oidc/code/query/query-beans.xml | 22 --
.../idp/flows/sp/consumer/oidc/oidc-beans.xml | 95 +++---
.../idp/flows/sp/consumer/oidc/oidc-flow.xml | 3 +-
.../idp/flows/sp/initiator/oidc/oidc-beans.xml | 28 +-
.../idp/flows/sp/initiator/oidc/oidc-flow.xml | 3 +-
.../shibboleth/idp/flows/sp/oidc-common-beans.xml | 28 +-
.../net/shibboleth/sp/service/agent/postconfig.xml | 14 +-
.../shibboleth/idp/module/conf/sp/oidc.properties | 9 +
.../oidc/flows/OIDCSessionInitiatorFlowTest.java | 1 +
.../sp/oidc/flows/OIDCTokenConsumerFlowTest.java | 350 ++++++++++++++++++---
.../shibboleth/sp/oidc/flows/TestConstants.java | 114 +++++--
...DCEnvironmentApplicationContextInitializer.java | 2 +
.../shibboleth/idp/module/conf/sp/sp.properties | 116 +++++--
sp-oidc-impl/pom.xml | 21 ++
.../impl/AuthTimeRequestedActivationCondition.java | 81 +++++
...thenticationRequestStateForStorageStrategy.java | 209 ++++++++++++
.../AuthenticationRequestTimeLookupFunction.java | 97 ++++++
...AgeFromAuthenticationRequestLookupFunction.java | 117 +++++++
...omAuthenticationRequestStateLookupStrategy.java | 87 +++++
.../impl/PrepareOIDCInboundMessageContext.java | 23 +-
.../profile/impl/ProcessCorrelationCookie.java | 55 +++-
...AuthenticationRequestToPeerContextConsumer.java | 170 ++++++++++
.../impl/SetNonceValueToTokenContextConsumer.java | 9 +-
.../AuthTimeRequestedActivationConditionTest.java | 102 ++++++
...ticationRequestStateForStorageStrategyTest.java | 180 +++++++++++
...uthenticationRequestTimeLookupFunctionTest.java | 111 +++++++
...romAuthenticationRequestLookupFunctionTest.java | 121 +++++++
...enticationRequestToPeerContextConsumerTest.java | 139 ++++++++
37 files changed, 2708 insertions(+), 222 deletions(-)
diff --git a/pom.xml b/pom.xml
index f44c83f..0a20d17 100644
--- a/pom.xml
+++ b/pom.xml
@@ -192,6 +192,14 @@ copyright/license information. -->
<type>pom</type>
<scope>import</scope>
</dependency>
+ <!-- Jackson BOM -->
+ <dependency>
+ <groupId>com.fasterxml.jackson</groupId>
+ <artifactId>jackson-bom</artifactId>
+ <version>${jackson.version}</version>
+ <type>pom</type>
+ <scope>import</scope>
+ </dependency>
<!-- Shibboleth IdP BOM for importing IdP dependencies -->
<dependency>
<groupId>${idp.groupId}</groupId>
diff --git a/sp-oidc-api/pom.xml b/sp-oidc-api/pom.xml
index af71cd6..26e3b02 100644
--- a/sp-oidc-api/pom.xml
+++ b/sp-oidc-api/pom.xml
@@ -79,6 +79,12 @@
</dependency>
<!-- Test Dependencies -->
+
+ <dependency>
+ <groupId>org.mockito</groupId>
+ <artifactId>mockito-core</artifactId>
+ <scope>test</scope>
+ </dependency>
</dependencies>
</project>
diff --git a/sp-oidc-api/src/main/java/net/shibboleth/sp/oidc/context/AuthnRequestStateDataContext.java b/sp-oidc-api/src/main/java/net/shibboleth/sp/oidc/context/AuthnRequestStateDataContext.java
new file mode 100644
index 0000000..9ed6356
--- /dev/null
+++ b/sp-oidc-api/src/main/java/net/shibboleth/sp/oidc/context/AuthnRequestStateDataContext.java
@@ -0,0 +1,50 @@
+/*
+ * 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;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.messaging.context.BaseContext;
+
+import net.shibboleth.sp.oidc.profile.AuthenticationRequestStateData;
+
+/**
+ * A context to hold recovered state information about the original authentication request.
+ */
+public class AuthnRequestStateDataContext extends BaseContext {
+
+ /** The authentication state data. */
+ @Nullable private AuthenticationRequestStateData authnState;
+
+ /**
+ * Sets the authentication state recovered from the authentication request.
+ *
+ * @param authnState The authnState to set.
+ */
+ @Nonnull public AuthnRequestStateDataContext setAuthnState(@Nullable final AuthenticationRequestStateData state) {
+ authnState = state;
+ return this;
+ }
+
+ /**
+ * Get the authentication state recovered from the authentication request.
+ *
+ * @return the authentication state.
+ */
+ @Nullable public AuthenticationRequestStateData getAuthnState() {
+ return authnState;
+ }
+
+}
diff --git a/sp-oidc-api/src/main/java/net/shibboleth/sp/oidc/context/navigate/AuthenticationRequestFromOutboundLookupStrategy.java b/sp-oidc-api/src/main/java/net/shibboleth/sp/oidc/context/navigate/AuthenticationRequestFromOutboundLookupStrategy.java
new file mode 100644
index 0000000..4b29a8b
--- /dev/null
+++ b/sp-oidc-api/src/main/java/net/shibboleth/sp/oidc/context/navigate/AuthenticationRequestFromOutboundLookupStrategy.java
@@ -0,0 +1,43 @@
+/*
+ * 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 org.opensaml.messaging.context.MessageContext;
+import org.opensaml.profile.context.ProfileRequestContext;
+
+import net.shibboleth.oidc.profile.core.OIDCAuthenticationRequest;
+
+/**
+ * Lookup strategy that returns the {@link OIDCAuthenticationRequest} from the outbound message context.
+ */
+public class AuthenticationRequestFromOutboundLookupStrategy
+ implements Function<ProfileRequestContext, OIDCAuthenticationRequest> {
+
+ /** {@inheritDoc} */
+ @Override
+ public OIDCAuthenticationRequest apply(final ProfileRequestContext prc) {
+ final MessageContext outbound = prc.getOutboundMessageContext();
+ if (outbound == null) {
+ return null;
+ }
+ if (outbound.getMessage() instanceof final OIDCAuthenticationRequest request) {
+ return request;
+ }
+ return null;
+ }
+
+}
diff --git a/sp-oidc-api/src/main/java/net/shibboleth/sp/oidc/context/navigate/ClientIdBiFunctionLookupStrategy.java b/sp-oidc-api/src/main/java/net/shibboleth/sp/oidc/context/navigate/ClientIdBiFunctionLookupStrategy.java
new file mode 100644
index 0000000..2c8138d
--- /dev/null
+++ b/sp-oidc-api/src/main/java/net/shibboleth/sp/oidc/context/navigate/ClientIdBiFunctionLookupStrategy.java
@@ -0,0 +1,55 @@
+/*
+ * 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.BiFunction;
+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 com.nimbusds.jwt.JWTClaimsSet;
+
+import net.shibboleth.shared.logic.Constraint;
+
+/**
+ * A strategy that adapts a {@link Function} into a {@link BiFunction} for clientId lookup.
+ */
+ at ThreadSafe
+public class ClientIdBiFunctionLookupStrategy implements BiFunction<ProfileRequestContext, JWTClaimsSet, String>{
+
+ /** The delegated to clientId lookup strategy.*/
+ @Nonnull private final Function<ProfileRequestContext, String> clientIdLookupStrategy;
+
+ /**
+ *
+ * Constructor.
+ *
+ * @param strategy the delegated clientIdLookupStrategy
+ */
+ public ClientIdBiFunctionLookupStrategy(@Nonnull final Function<ProfileRequestContext, String> strategy) {
+ clientIdLookupStrategy = Constraint.isNotNull(strategy, "ClientID Lookup Strategy should not be null");
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public String apply(@Nullable final ProfileRequestContext prc, @Nullable final JWTClaimsSet claimsSet) {
+ return clientIdLookupStrategy.apply(prc);
+ }
+
+}
diff --git a/sp-oidc-api/src/main/java/net/shibboleth/sp/oidc/profile/AuthenticationRequestStateData.java b/sp-oidc-api/src/main/java/net/shibboleth/sp/oidc/profile/AuthenticationRequestStateData.java
new file mode 100644
index 0000000..a0b3e6e
--- /dev/null
+++ b/sp-oidc-api/src/main/java/net/shibboleth/sp/oidc/profile/AuthenticationRequestStateData.java
@@ -0,0 +1,303 @@
+/*
+ * 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;
+
+
+import java.time.Duration;
+import java.time.Instant;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.NotThreadSafe;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.nimbusds.openid.connect.sdk.OIDCClaimsRequest;
+import com.nimbusds.openid.connect.sdk.claims.ClaimRequirement;
+import com.nimbusds.openid.connect.sdk.claims.ClaimsSetRequest;
+import com.nimbusds.openid.connect.sdk.claims.ClaimsSetRequest.Entry;
+
+import net.shibboleth.oidc.profile.core.OIDCAuthenticationRequest;
+
+/**
+ * A DTO class that carries authentication request information that needs to be recovered to validate the authentication
+ * response. This class is designed for JSON serialization and deserialization for storage e.g. in a cookie.
+ */
+ at JsonInclude(JsonInclude.Include.NON_NULL)
+ at NotThreadSafe
+public class AuthenticationRequestStateData {
+
+ /** The identifier of the client that is making the authentication request. */
+ @Nullable private String clientId;
+
+ /** The id_token nonce value. */
+ @Nullable private String nonce;
+
+ /** The expected issuer of the authentication response. */
+ @Nullable private String authenticatingAuthority;
+
+ /** The optional PKCE code verifier. */
+ @Nullable private String pkceCodeVerifier;
+
+ /** The optional max authentication age. */
+ @Nullable private Duration maxAge;
+
+ /**
+ * Has auth_time been requested and is required inside the id_token? either because the max_age parameter was used,
+ * or the claims parameter requested auth_time.
+ */
+ private boolean authTimeRequired;
+
+ /** The time at which the RP made the authentication request to the OP.*/
+ @Nullable private Instant authnRequestTime;
+
+ /**
+ * Set the identifier of the client that is making the authentication request. Can be used to ensure the audience
+ * of the response matches the client that made the request.
+ *
+ * @param id The client ID to set.
+ *
+ * @return the updated object
+ */
+ @Nonnull public AuthenticationRequestStateData setClientId(@Nullable final String id) {
+ clientId = id;
+ return this;
+ }
+
+ /**
+ * Get the identifier of the client that is making the authentication request. Can be used to ensure the audience
+ * of the response matches the client that made the request.
+ *
+ * @return the client ID.
+ */
+ @Nullable public String getClientId() {
+ return clientId;
+ }
+
+ /**
+ * Get the nonce expected in the id_token response.
+ *
+ * @return the nonce
+ */
+ @JsonProperty("nonce")
+ @Nullable public String getNonce() {
+ return nonce;
+ }
+
+ /**
+ * Set the nonce expected in the id_token response.
+ *
+ * @param nonceIn the nonce to set
+ *
+ * @return the updated object
+ */
+ @Nonnull public AuthenticationRequestStateData setNonce(@Nullable final String nonceIn) {
+ nonce = nonceIn;
+ return this;
+ }
+
+ /**
+ * Get the expected issuer of the authentication response.
+ *
+ * @return the authentication authority
+ */
+ @JsonProperty("authority")
+ @Nullable public String getAuthenticationAuthority() {
+ return authenticatingAuthority;
+ }
+
+ /**
+ * Set the expected issuer of the authentication response.
+ *
+ * @param issuer the authentication authority to set
+ *
+ * @return the updated object
+ */
+ @Nonnull public AuthenticationRequestStateData setAuthenticationAuthority(@Nullable final String authority) {
+ authenticatingAuthority = authority;
+ return this;
+ }
+
+ /**
+ * Get the PKCE code verifier used to generate the code challenge in the authentication request.
+ *
+ * @return the PKCE code verifier
+ */
+ @JsonProperty("pkce_code_verifier")
+ @Nullable public String getPkceCodeVerifier() {
+ return pkceCodeVerifier;
+ }
+
+ /**
+ * Set the PKCE code verifier used to generate the code challenge in the authentication request.
+ *
+ * @param pkceCodeVerifier the PKCE code verifier
+ *
+ * @return the updated object
+ */
+ @Nonnull public AuthenticationRequestStateData setPkceCodeVerifier(@Nullable final String pkceCodeVerifierIn) {
+ pkceCodeVerifier = pkceCodeVerifierIn;
+ return this;
+ }
+
+ /**
+ * Set the maxAge, a null value indicates no maxAge was specified in the authentication request.
+ *
+ * @return the maxAge.
+ */
+ @JsonProperty("max_age")
+ public Duration getMaxAge() {
+ return maxAge;
+ }
+
+ /**
+ * set the maxAge, a null value indicates no maxAge was specified in the authentication request.
+ *
+ * @param age The max_age to set.
+ */
+ @Nonnull public AuthenticationRequestStateData setMaxAge(@Nullable final Duration age) {
+ maxAge = age;
+ return this;
+ }
+
+ /**
+ * Set if auth_time is required inside the id_token either because the max_age parameter was used, or the claims
+ * parameter requested it.
+ *
+ * @param requested is the auth_time requested and hence required inside the id_token.
+ */
+ @Nonnull public AuthenticationRequestStateData setAuthTimeRequired(final boolean requested) {
+ authTimeRequired = requested;
+ return this;
+ }
+
+ /**
+ * Is auth_time required inside the id_token? either because the max_age parameter was used, or the claims parameter
+ * requested it.
+ *
+ * @return true if auth_time has been requested and is required inside the id_token, false otherwise.
+ */
+ @JsonProperty("is_auth_time_required")
+ public boolean isAuthTimeRequired() {
+ return authTimeRequired;
+ }
+
+ /**
+ * Set the time at which this RP sent this authentication request to the OP.
+ *
+ * @param time the time the request was made
+ */
+ public AuthenticationRequestStateData setAuthnRequestTime(@Nullable final Instant time) {
+ authnRequestTime = time;
+ return this;
+ }
+
+ /**
+ * Get the time at which this RP sent this authentication request to the OP.
+ *
+ * @return the time the request was made
+ */
+ @JsonProperty("authn_request_time")
+ @Nullable public Instant getAuthnRequestTime() {
+ return authnRequestTime;
+ }
+
+ /**
+ * Create an instance of this class from the given authentication request and authentication authority.
+ *
+ * @param request the authentication request
+ * @param authenticationAuthority the expected issuer of the authentication response
+ *
+ * @return a populated instance of this class
+ */
+ public static AuthenticationRequestStateData from(
+ @Nonnull final OIDCAuthenticationRequest request, @Nonnull final String authenticationAuthority) {
+
+ @SuppressWarnings("null")
+ final AuthenticationRequestStateData state = new AuthenticationRequestStateData()
+ .setNonce(request.getNonce() != null ? request.getNonce().getValue() : null)
+ .setAuthenticationAuthority(authenticationAuthority)
+ .setPkceCodeVerifier(request.getCodeVerifier())
+ .setMaxAge(request.getMaxAge())
+ .setAuthnRequestTime(request.getAuthnRequestTime())
+ .setClientId(request.getClientID().getValue());
+
+ state.setAuthTimeRequired(isAuthTimeRequired(request));
+ return state;
+ }
+
+ /**
+ * Determine if auth_time is required inside the id_token either because the max_age parameter was used, or the
+ * claims parameter requested it as an ESSENTIAL claim.
+ *
+ * @param request the authentication request
+ *
+ * @return true if auth_time is required inside the id_token, false otherwise.
+ */
+ static boolean isAuthTimeRequired(@Nonnull final OIDCAuthenticationRequest request) {
+
+ // If max_age is specified, auth_time MUST be included in the ID Token.
+ if (request.getMaxAge() != null) {
+ return true;
+ }
+
+ final OIDCClaimsRequest requestedClaims = request.getRequestedClaims();
+ if (requestedClaims == null) {
+ return false;
+ }
+
+ final ClaimsSetRequest idTokenClaims = requestedClaims.getIDTokenClaimsRequest();
+ if (idTokenClaims == null) {
+ return false;
+ }
+ // If auth_time is requested as an ESSENTIAL claim, auth_time MUST be included in the ID Token.
+ final Entry authTime = idTokenClaims.get("auth_time");
+ return authTime != null && authTime.getClaimRequirement() == ClaimRequirement.ESSENTIAL;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public String toString() {
+
+ final String nonceSuffix = mask(nonce);
+ String pkceCodeVerifierSuffix = mask(pkceCodeVerifier);
+ pkceCodeVerifierSuffix = pkceCodeVerifierSuffix == null ? "" : pkceCodeVerifierSuffix;
+
+ return "AuthenticationRequestStateData [nonce="+nonceSuffix+", authenticatingAuthority=" + authenticatingAuthority
+ + ", pkceCodeVerifier="+pkceCodeVerifierSuffix+", maxAge=" + maxAge + ", authTimeRequired="
+ + authTimeRequired + ", authnRequestTime=" + authnRequestTime + "]";
+ }
+
+ /**
+ * A simple method to mask all but the last 2 characters of a string value for logging purposes.
+ *
+ * @param value the string value to mask
+ * @return the masked string
+ */
+ private static String mask(final String value) {
+ if (value == null) {
+ return null;
+ }
+ if (value.length() <= 4) {
+ return "****";
+ }
+ return "****" + value.substring(value.length() - 2);
+ }
+
+
+
+
+}
+
diff --git a/sp-oidc-api/src/test/java/net/shibboleth/sp/oidc/profile/AuthenticationRequestStateDataTest.java b/sp-oidc-api/src/test/java/net/shibboleth/sp/oidc/profile/AuthenticationRequestStateDataTest.java
new file mode 100644
index 0000000..0a1cd5d
--- /dev/null
+++ b/sp-oidc-api/src/test/java/net/shibboleth/sp/oidc/profile/AuthenticationRequestStateDataTest.java
@@ -0,0 +1,134 @@
+/*
+ * 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;
+
+import static org.testng.Assert.assertFalse;
+import static org.testng.Assert.assertTrue;
+
+import java.time.Duration;
+
+import org.mockito.Mockito;
+import org.testng.annotations.Test;
+
+import com.nimbusds.openid.connect.sdk.OIDCClaimsRequest;
+import com.nimbusds.openid.connect.sdk.claims.ClaimRequirement;
+import com.nimbusds.openid.connect.sdk.claims.ClaimsSetRequest;
+import com.nimbusds.openid.connect.sdk.claims.ClaimsSetRequest.Entry;
+
+import net.shibboleth.oidc.profile.core.OIDCAuthenticationRequest;
+
+/**
+ * Tests for {@link AuthenticationRequestStateData}.
+ */
+public class AuthenticationRequestStateDataTest {
+
+ @Test
+ void testAuthTimeNotRequired_whenNoMaxAgeAndNoClaims() {
+ final OIDCAuthenticationRequest request = Mockito.mock(OIDCAuthenticationRequest.class);
+ Mockito.when(request.getMaxAge()).thenReturn(null);
+ Mockito.when(request.getRequestedClaims()).thenReturn(null);
+
+ assertFalse(AuthenticationRequestStateData.isAuthTimeRequired(request));
+ }
+
+ @Test
+ void testAuthTimeNotRequired_whenIdTokenClaimsWithoutAuthTime() {
+ final OIDCAuthenticationRequest request = Mockito.mock(OIDCAuthenticationRequest.class);
+ final OIDCClaimsRequest claims = Mockito.mock(OIDCClaimsRequest.class);
+ final ClaimsSetRequest idTokenClaims = new ClaimsSetRequest();
+
+ Mockito.when(request.getMaxAge()).thenReturn(null);
+ Mockito.when(request.getRequestedClaims()).thenReturn(claims);
+ Mockito.when(claims.getIDTokenClaimsRequest()).thenReturn(idTokenClaims);
+
+ assertFalse(AuthenticationRequestStateData.isAuthTimeRequired(request));
+ }
+
+
+ @Test
+ void testAuthTimeNotRequired_whenAuthTimeNotEssential() {
+ final OIDCAuthenticationRequest request = Mockito.mock(OIDCAuthenticationRequest.class);
+ final ClaimsSetRequest idTokenClaims = new ClaimsSetRequest();
+ final OIDCClaimsRequest claims = new OIDCClaimsRequest().withIDTokenClaimsRequest
+ (idTokenClaims.add(new Entry("auth_time").withClaimRequirement(ClaimRequirement.VOLUNTARY)));
+
+ Mockito.when(request.getMaxAge()).thenReturn(null);
+ Mockito.when(request.getRequestedClaims()).thenReturn(claims);
+
+ assertFalse(AuthenticationRequestStateData.isAuthTimeRequired(request));
+ }
+
+ @Test
+ void testAuthTimeRequired_whenAuthTimeEssential() {
+ final OIDCAuthenticationRequest request = Mockito.mock(OIDCAuthenticationRequest.class);
+ final ClaimsSetRequest idTokenClaims = new ClaimsSetRequest();
+ final OIDCClaimsRequest claims = new OIDCClaimsRequest().withIDTokenClaimsRequest
+ (idTokenClaims.add(new Entry("auth_time").withClaimRequirement(ClaimRequirement.ESSENTIAL)));
+
+
+ Mockito.when(request.getMaxAge()).thenReturn(null);
+ Mockito.when(request.getRequestedClaims()).thenReturn(claims);
+
+ assertTrue(AuthenticationRequestStateData.isAuthTimeRequired(request));
+ }
+
+ @Test
+ void testAuthTimeRequired_whenMaxAgePresent() {
+ final OIDCAuthenticationRequest request = Mockito.mock(OIDCAuthenticationRequest.class);
+
+ Mockito.when(request.getMaxAge()).thenReturn(Duration.ofSeconds(60));
+ Mockito.when(request.getRequestedClaims()).thenReturn(null);
+
+ assertTrue(AuthenticationRequestStateData.isAuthTimeRequired(request));
+ }
+
+ @Test
+ void testAuthTimeRequired_whenMaxAgePresentEvenIfAuthTimeNotEssential() {
+ final OIDCAuthenticationRequest request = Mockito.mock(OIDCAuthenticationRequest.class);
+ final ClaimsSetRequest idTokenClaims = new ClaimsSetRequest();
+ final OIDCClaimsRequest claims = new OIDCClaimsRequest().withIDTokenClaimsRequest
+ (idTokenClaims.add(new Entry("auth_time").withClaimRequirement(ClaimRequirement.VOLUNTARY)));
+
+ Mockito.when(request.getMaxAge()).thenReturn(Duration.ofSeconds(60));
+ Mockito.when(request.getRequestedClaims()).thenReturn(claims);
+
+ assertTrue(AuthenticationRequestStateData.isAuthTimeRequired(request));
+ }
+
+ @Test
+ void testAuthTimeRequired_whenMaxAgeAndAuthTimeEssential() {
+ final OIDCAuthenticationRequest request = Mockito.mock(OIDCAuthenticationRequest.class);
+ final ClaimsSetRequest idTokenClaims = new ClaimsSetRequest();
+ final OIDCClaimsRequest claims = new OIDCClaimsRequest().withIDTokenClaimsRequest
+ (idTokenClaims.add(new Entry("auth_time").withClaimRequirement(ClaimRequirement.ESSENTIAL)));
+
+ Mockito.when(request.getMaxAge()).thenReturn(Duration.ofSeconds(60));
+ Mockito.when(request.getRequestedClaims()).thenReturn(claims);
+
+ assertTrue(AuthenticationRequestStateData.isAuthTimeRequired(request));
+ }
+
+ @Test
+ void testFromCopiesAuthTimeRequiredCorrectly() {
+ final OIDCAuthenticationRequest request = Mockito.mock(OIDCAuthenticationRequest.class);
+ Mockito.when(request.getMaxAge()).thenReturn(Duration.ofSeconds(60));
+
+ final AuthenticationRequestStateData state =
+ AuthenticationRequestStateData.from(request, "https://op.example.org");
+
+ assertTrue(state.isAuthTimeRequired());
+ }
+
+}
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 ef54bda..6a74492 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
@@ -131,7 +131,5 @@
</constructor-arg>
</bean>
- <!-- TODO Does this need to load in the global context? -->
- <bean id="shibboleth.sp.oidc.DefaultJSONObjectMapper" class="com.fasterxml.jackson.databind.ObjectMapper" />
</beans>
diff --git a/sp-oidc-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/consumer/oidc/code/post/post-beans.xml b/sp-oidc-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/consumer/oidc/code/post/post-beans.xml
index 2010c5b..e5743d9 100644
--- a/sp-oidc-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/consumer/oidc/code/post/post-beans.xml
+++ b/sp-oidc-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/consumer/oidc/code/post/post-beans.xml
@@ -6,28 +6,6 @@
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util.xsd"
default-init-method="initialize" default-destroy-method="destroy">
-
- <!-- <alias alias="shibboleth.HttpServletRequestValidator" name="shibboleth.HttpServletRequestValidator.BasicParams.SAML2.Artifact" /> -->
-
-<!-- <bean id="DecodeMessage" class="net.shibboleth.sp.profile.impl.DecodeMessage" scope="prototype"
- p:messageType="org.opensaml.saml.saml2.core.Response">
- <constructor-arg>
- <bean class="org.opensaml.saml.saml2.binding.decoding.impl.HTTPArtifactDecoder" scope="prototype"
- p:checkDuringInit="false"
- p:parserPool-ref="shibboleth.ParserPool"
- p:httpServletRequestSupplier-ref="shibboleth.RemotedHttpServletRequestSupplier"
- p:bindingDescriptor-ref="shibboleth.Binding.SAML2Artifact"
- p:peerEntityRole="#{T(org.opensaml.saml.saml2.metadata.IDPSSODescriptor).DEFAULT_ELEMENT_NAME}"
- p:roleDescriptorResolver-ref="shibboleth.RoleDescriptorResolver"
- p:identifierGenerationStrategy-ref="shibboleth.DefaultIdentifierGenerationStrategy"
- p:selfEntityIDResolver-ref="ArtifactSupport.SelfEntityIDResolver"
- p:artifactEndpointResolver-ref="shibboleth.EndpointResolver"
- p:SOAPClient-ref="shibboleth.SOAPClient.SAML"
- p:SOAPPipelineName="SP.SAML2.ArtifactDecoder"
- p:SOAPClientSecurityConfigurationProfileId="#{T(net.shibboleth.saml.saml2.profile.config.BrowserSSOProfileConfiguration).PROFILE_ID}"
- />
- </constructor-arg>
- </bean> -->
<bean id="DecodeMessage" class="net.shibboleth.sp.profile.impl.DecodeMessage" scope="prototype"
p:messageType="org.opensaml.saml.saml2.core.Response">
diff --git a/sp-oidc-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/consumer/oidc/code/query/query-beans.xml b/sp-oidc-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/consumer/oidc/code/query/query-beans.xml
index 3a15b80..3a1ba1d 100644
--- a/sp-oidc-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/consumer/oidc/code/query/query-beans.xml
+++ b/sp-oidc-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/consumer/oidc/code/query/query-beans.xml
@@ -6,28 +6,6 @@
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util.xsd"
default-init-method="initialize" default-destroy-method="destroy">
-
- <!-- <alias alias="shibboleth.HttpServletRequestValidator" name="shibboleth.HttpServletRequestValidator.BasicParams.SAML2.Artifact" /> -->
-
-<!-- <bean id="DecodeMessage" class="net.shibboleth.sp.profile.impl.DecodeMessage" scope="prototype"
- p:messageType="org.opensaml.saml.saml2.core.Response">
- <constructor-arg>
- <bean class="org.opensaml.saml.saml2.binding.decoding.impl.HTTPArtifactDecoder" scope="prototype"
- p:checkDuringInit="false"
- p:parserPool-ref="shibboleth.ParserPool"
- p:httpServletRequestSupplier-ref="shibboleth.RemotedHttpServletRequestSupplier"
- p:bindingDescriptor-ref="shibboleth.Binding.SAML2Artifact"
- p:peerEntityRole="#{T(org.opensaml.saml.saml2.metadata.IDPSSODescriptor).DEFAULT_ELEMENT_NAME}"
- p:roleDescriptorResolver-ref="shibboleth.RoleDescriptorResolver"
- p:identifierGenerationStrategy-ref="shibboleth.DefaultIdentifierGenerationStrategy"
- p:selfEntityIDResolver-ref="ArtifactSupport.SelfEntityIDResolver"
- p:artifactEndpointResolver-ref="shibboleth.EndpointResolver"
- p:SOAPClient-ref="shibboleth.SOAPClient.SAML"
- p:SOAPPipelineName="SP.SAML2.ArtifactDecoder"
- p:SOAPClientSecurityConfigurationProfileId="#{T(net.shibboleth.saml.saml2.profile.config.BrowserSSOProfileConfiguration).PROFILE_ID}"
- />
- </constructor-arg>
- </bean> -->
<bean id="DecodeMessage" class="net.shibboleth.sp.profile.impl.DecodeMessage" scope="prototype"
p:messageType="com.nimbusds.openid.connect.sdk.AuthenticationResponse">
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 f1de866..7cdcd9f 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
@@ -44,21 +44,28 @@
p:cookieManager-ref="shibboleth.sp.RemotedCookieManager"
p:cookiePrefix="%{sp.correlation.cookiePrefix:__Host-_shibsp_req_}"
p:createOutputObjects="true"
+ p:errorFatal="true"
p:stateTokenLookupStrategy-ref="StateFromStateLookup">
<property name="cookieValueConsumerStrategy">
<bean class="net.shibboleth.sp.oidc.profile.impl.SetNonceValueToTokenContextConsumer"/>
</property>
</bean>
-
- <bean id="ProcessIssuerCorrelationCookie" class="net.shibboleth.sp.oidc.profile.impl.ProcessCorrelationCookie" scope="prototype"
+
+ <!-- TODO should this be a cookie or go through the state manager-->
+ <bean id="ProcessAuthnStateCookie" class="net.shibboleth.sp.oidc.profile.impl.ProcessCorrelationCookie" scope="prototype"
p:cookieManager-ref="shibboleth.sp.RemotedCookieManager"
- p:cookiePrefix="%{sp.correlation.cookiePrefix:__Host-_shibsp_iss_}"
+ p:cookiePrefix="%{sp.correlation.cookiePrefix:__Host-_shibsp_authnstate_}"
p:createOutputObjects="true"
+ p:errorFatal="true"
p:stateTokenLookupStrategy-ref="StateFromStateLookup">
<property name="cookieValueConsumerStrategy">
- <bean class="net.shibboleth.sp.oidc.profile.impl.SetIssuerToPeerContextConsumer"/>
- </property>
- </bean>
+ <bean class="net.shibboleth.sp.oidc.profile.impl.SetAuthenticationRequestToPeerContextConsumer"
+ p:objectMapper-ref="shibboleth.JSONObjectMapper"
+ p:dataSealer-ref="#{'%{sp.dataSealer:shibboleth.DataSealer}'.trim()}"
+ p:unsealStatePredicate="#{getObject('%{sp.oidc.sealAuthenticationStatePredicate:}') ?: ((%{sp.oidc.sealAuthenticationState:true}) ? getObject('shibboleth.Conditions.TRUE') : getObject('shibboleth.Conditions.FALSE'))}"
+ />
+ </property>
+ </bean>
<bean id="ValidateAuthenticationResponseResult" scope="prototype"
class="net.shibboleth.sp.oidc.profile.impl.ValidateAuthenticationResponseResult" />
@@ -67,12 +74,6 @@
class="net.shibboleth.sp.oidc.profile.impl.ValidateResponseState"
p:nonceTokenLookupStrategy-ref="NonceFromStateLookup" />
- <!-- TODO Copied over -->
- <bean id="PrepareInboundMessageContext"
- class="net.shibboleth.sp.oidc.profile.impl.PrepareOIDCInboundMessageContext" scope="prototype"
- p:identifierLookupStrategy-ref="shibboleth.RelyingPartyIdLookup.Simple">
- </bean>
-
<bean id="ProviderMetadataLookup" parent="WebFlowInboundMessageHandlerAdaptor">
<constructor-arg name="messageHandler"> <!-- TODO Copied over -->
<bean class="net.shibboleth.sp.oidc.metadata.impl.OIDCProviderMetadataLookupHandler"
@@ -92,8 +93,7 @@
<bean id="SelectProfileConfiguration"
class="net.shibboleth.idp.profile.impl.SelectProfileConfiguration" scope="prototype"
- p:profileId-ref="shibboleth.sp.oidc.ProfileId" />
-
+ p:profileId-ref="shibboleth.sp.oidc.ProfileId" />
<bean id="NonceFromStateLookup" parent="shibboleth.Functions.Compose">
<constructor-arg name="g">
@@ -118,7 +118,9 @@
<property name="handlers">
<list>
<bean id="InitializeOAuth2ClientAuthenticationContext" scope="prototype"
- class="net.shibboleth.oidc.profile.impl.InitializeOAuth2ClientAuthenticationContext"/>
+ class="net.shibboleth.oidc.profile.impl.InitializeOAuth2ClientAuthenticationContext"
+ p:OAuth2ClientAuthenticationContextLookupStrategy-ref="shibboleth.ChildLookupOrCreate.OAuth2ClientAuthenticationContextFromOIDCPeer"/>
+
<bean id="PopulateJWTClientAuthenticationSignatureSigningParameters" scope="prototype"
class="net.shibboleth.oidc.profile.impl.PopulateJWTSignatureSigningParametersHandler"
p:noResultIsError="true"
@@ -144,6 +146,7 @@
</bean>
</property>
</bean>
+
<bean id="InitializeOAuth2ClientAuthenticationMethodHandler" scope="prototype"
class="net.shibboleth.sp.oidc.profile.impl.InitializeOAuth2ClientAuthenticationMethodHandler"
p:securityParametersContextLookupStrategy-ref="shibboleth.ChildLookupOrCreate.SecurityParametersFromOAuth2ClientAuthenticationContext"
@@ -166,7 +169,7 @@
<bean id="DefaultTokenResponseDecoder" scope="prototype"
class="net.shibboleth.oidc.profile.decoding.impl.AccessTokenResponseDecoder"
- p:objectMapper="#{getObject('%{sp.oidc.jsonObjectMapper:}') ?: getObject('shibboleth.sp.oidc.DefaultJSONObjectMapper')}" />
+ p:objectMapper="#{getObject('%{sp.oidc.jsonObjectMapper:}') ?: getObject('shibboleth.JSONObjectMapper')}" />
<bean id="DefaultAuthCodeTokenResponseEncoder" scope="prototype"
class="net.shibboleth.oidc.profile.encoding.impl.AuthCodeTokenRequestEncoder"
@@ -261,16 +264,16 @@
<util:list id="IDTokenClaimsValidators" value-type="net.shibboleth.oidc.jwt.claims.ClaimsValidator">
<ref bean="IDTokenRequiredClaimsValidator" />
<ref bean="IssuerClaimsValidator" />
- <!-- <ref bean="AudienceClaimsValidator" /> -->
+ <ref bean="AudienceClaimsValidator" />
<ref bean="AzpClaimRequiredValidator" />
- <!-- ref bean="AzpClaimsValidator" /> -->
+ <ref bean="AzpClaimsValidator" />
<ref bean="ExpiryClaimsValidator" />
<ref bean="IssuedAtClaimsValidator" />
<ref bean="NotBeforeClaimsValidator" />
-<!-- <ref bean="NonceClaimValidator" /> -->
+ <ref bean="NonceClaimValidator" />
<ref bean="AtHashValidator"/>
-<!-- <ref bean="AuthenticationTimeClaimValidator"/> -->
-<!-- <ref bean="ACRClaimValidator"/> -->
+ <ref bean="AuthenticationTimeClaimValidator"/>
+ <!-- <ref bean="ACRClaimValidator"/> should we just pass these back to the agent-->
</util:list>
<bean id="IDTokenRequiredClaimsValidator"
@@ -318,20 +321,20 @@
<bean id="ManyValuesPredicate"
class="net.shibboleth.oidc.security.jwt.claims.impl.ManyValuesIntegerComparisonPredicate" />
-<!-- <bean id="AzpClaimsValidator" class="net.shibboleth.oidc.security.jwt.claims.impl.ExactMatchClaimsValidator"
- p:claimName="azp" p:valueToMatchLookupStrategy-ref="ClientIDFromOAuth2ClientContextFunction">
+ <bean id="AzpClaimsValidator" class="net.shibboleth.oidc.security.jwt.claims.impl.ExactMatchClaimsValidator"
+ p:claimName="azp" p:valueToMatchLookupStrategy-ref="shibboleth.ClientIdLookup.BiFunction">
<property name="activationCondition">
<bean id="AzpClaimExistsCondition"
class="net.shibboleth.oidc.security.jwt.claims.impl.ClaimExistsActivationCondition" c:claimToCheck="azp" />
</property>
- </bean> -->
+ </bean>
<bean id="IssuerIDFromOIDCProviderMetadataContextLookupFunction" scope="prototype"
class="net.shibboleth.oidc.profile.logic.IssuerIDFromOIDCProviderMetadataContextLookupFunction"
p:oIDCMetadataContextLookupStrategy-ref="shibboleth.ChildLookup.OIDCProviderMetadataContextFromInboundMessageContext" />
<bean id="AudienceClaimsValidator" class="net.shibboleth.oidc.security.jwt.claims.impl.AudienceClaimsValidator"
- p:audienceLookupStrategy-ref="ClientIDFromOAuth2ClientContextFunction"
+ p:audienceLookupStrategy-ref="shibboleth.ClientIdLookup.BiFunction"
p:extraAudienceValidation="true">
<property name="additionalAudiencesLookupStrategy">
<bean class="net.shibboleth.oidc.security.jwt.claims.impl.ExtraAudiencesLookupStrategy"
@@ -343,18 +346,18 @@
class="net.shibboleth.oidc.profile.context.navigate.ClientIDFromOAuth2ClientContextFunction"
c:oauth2ClientContextLookupStrategy-ref="shibboleth.ChildLookup.OAuth2ClientContextFromInbound" />
-<!-- <bean id="NonceClaimValidator" class="net.shibboleth.oidc.security.jwt.claims.impl.ExactMatchClaimsValidator"
+ <bean id="NonceClaimValidator" class="net.shibboleth.oidc.security.jwt.claims.impl.ExactMatchClaimsValidator"
p:claimName="nonce"
p:valueToMatchLookupStrategy="#{getObject('sp.oidc.jwt.NonceLookupStrategy') ?:
getObject('DefaultNonceLookupStrategy')}"
p:activationCondition="#{getObject('sp.oidc.jwt.NonceActivationCondition') ?:
- getObject('DefaultNonceActivationCondition')}" /> -->
+ getObject('DefaultNonceActivationCondition')}" />
-<!-- <bean id="DefaultNonceActivationCondition"
- class="net.shibboleth.idp.plugin.authn.oidc.rp.security.impl.NonceValidationActivationCondition" /> -->
+ <bean id="DefaultNonceActivationCondition"
+ class="net.shibboleth.oidc.security.jwt.claims.impl.NonceValidationActivationCondition" />
-<!-- <bean id="DefaultNonceLookupStrategy"
- class="net.shibboleth.idp.plugin.authn.oidc.rp.security.impl.AuthenticationRequestNonceClaimLookupStrategy" /> -->
+ <bean id="DefaultNonceLookupStrategy"
+ class="net.shibboleth.sp.oidc.profile.impl.NonceFromAuthenticationRequestStateLookupStrategy" />
<bean id="OIDCMetadataContextChildLookup" class="org.opensaml.messaging.context.navigate.ChildContextLookup"
c:type="#{ T(net.shibboleth.oidc.metadata.context.OIDCMetadataContext) }" />
@@ -375,34 +378,36 @@
<bean id="DefaultIDTokenJOSEHeaderLookupStrategy"
class="net.shibboleth.oidc.profile.context.navigate.IDTokenJOSEHeaderLookupStrategy"/>
-<!-- <bean id="AuthenticationTimeClaimValidator"
+ <bean id="AuthenticationTimeClaimValidator"
class="net.shibboleth.oidc.security.jwt.claims.impl.AuthenticationTimeClaimsValidator"
p:authnLifetimeLookupStrategy-ref="MaxAgeLookupFunction"
p:authnRequestTimeLookupStrategy-ref="AuthenticationRequestTimeLookupFunction"
p:clockSkew="%{sp.oidc.idtoken.jwt.verifier.clockSkew:PT60S}"
p:activationCondition="#{getObject('sp.oidc.jwt.AuthTimeActivationCondition') ?:
- getObject('DefaultAuthTimeActivationCondition')}"/> -->
+ getObject('DefaultAuthTimeActivationCondition')}"/>
- <bean id="MaxAgeLookupFunction" scope="prototype"
- class="net.shibboleth.oidc.security.jwt.claims.impl.MaxAgeLookupFunction"
+ <bean id="MaxAgeLookupFunction" scope="prototype"
+ class="net.shibboleth.sp.oidc.profile.impl.MaxAgeFromAuthenticationRequestLookupFunction"
c:maxAgeDefault="%{sp.oidc.idtoken.jwt.verifier.authnLifetime:PT60S}"/>
-<!-- <bean id="AuthenticationRequestTimeLookupFunction"
- class="net.shibboleth.idp.plugin.authn.oidc.rp.context.navigate.AuthenticationRequestTimeLookupFunction"
- p:clockSkew="%{sp.oidc.idtoken.jwt.verifier.authnRequestClockSkew:PT0S}"/> -->
+ <bean id="AuthenticationRequestTimeLookupFunction"
+ class="net.shibboleth.sp.oidc.profile.impl.AuthenticationRequestTimeLookupFunction"
+ p:clockSkew="%{sp.oidc.idtoken.jwt.verifier.authnRequestClockSkew:PT0S}"
+ p:authenticationRequestLookupStrategy-ref="shibboleth.ChildLookup.AuthenticationRequestStateDataFromInbound"/>
-<!-- <bean id="DefaultAuthTimeActivationCondition"
- class="net.shibboleth.oidc.security.jwt.claims.impl.AuthTimeRequestedActivationCondition"
- c:authenticationRequestLookupStrategy-ref="shibboleth.ChildLookup.MessageLookup.Outbound.OIDCAuthenticationRequest"/> -->
+ <bean id="DefaultAuthTimeActivationCondition"
+ class="net.shibboleth.sp.oidc.profile.impl.AuthTimeRequestedActivationCondition"
+ c:authenticationRequestLookupStrategy-ref="shibboleth.ChildLookup.AuthenticationRequestStateDataFromInbound"/>
-<!-- <bean id="ACRClaimValidator" class="net.shibboleth.oidc.security.jwt.claims.impl.ACRClaimsValidator"
+
+ <!-- <bean id="ACRClaimValidator" class="net.shibboleth.oidc.security.jwt.claims.impl.ACRClaimsValidator"
p:requestedEssentialAcrsClaimLookupStrategy="#{getObject('sp.oidc.jwt.RequestedEssentialAcrsClaimLookupStrategy') ?:
getObject('DefaultRequestedEssentialAcrsClaimLookupStrategy')}"/>
<bean id="DefaultRequestedEssentialAcrsClaimLookupStrategy"
class="net.shibboleth.oidc.security.jwt.claims.impl.RequestedEssentialACRClaimsLookupStrategy"
- c:authenticationRequestLookupStrategy-ref="shibboleth.ChildLookup.MessageLookup.Outbound.OIDCAuthenticationRequest"/> -->
-
+ c:authenticationRequestLookupStrategy-ref="shibboleth.ChildLookup.MessageLookup.Outbound.OIDCAuthenticationRequest"/>
+ -->
<!-- - End ID Token Claims Validation -->
<bean id="CheckUserInfoRequiredCondition" class=" net.shibboleth.oidc.profile.config.logic.UserInfoLookupPredicate" />
@@ -417,7 +422,7 @@
<bean id="DefaultUserInfoResponseDecoder" scope="prototype"
class="net.shibboleth.oidc.profile.decoding.impl.UserInfoResponseDecoder"
- p:objectMapper="#{getObject('%{sp.oidc.jsonObjectMapper:}') ?: getObject('shibboleth.sp.oidc.DefaultJSONObjectMapper')}"/>
+ p:objectMapper="#{getObject('%{sp.oidc.jsonObjectMapper:}') ?: getObject('shibboleth.JSONObjectMapper')}"/>
<bean id="DefaultUserInfoRequestEncoder" scope="prototype"
class="net.shibboleth.oidc.profile.encoding.impl.UserInfoRequestEncoder"
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 7cc51ec..24eb746 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
@@ -8,7 +8,7 @@
<evaluate expression="DecodeMessage" />
<evaluate expression="MapStateTokenToResource" />
<evaluate expression="ProcessNonceCorrelationCookie" />
- <evaluate expression="ProcessIssuerCorrelationCookie" />
+ <evaluate expression="ProcessAuthnStateCookie" />
<evaluate expression="ValidateResponseStateMatchesRequest" />
<evaluate expression="ValidateAuthenticationResponseResult" />
@@ -22,7 +22,6 @@
<transition on="proceed" to="AuthorizationCodeFlow" />
</action-state>
- <!-- TODO Single grant type supported for now, but we could switch before this like the RP -->
<action-state id="AuthorizationCodeFlow">
<evaluate expression="InitializeOAuth2ClientAuthenticationContextHandler" />
<evaluate expression="ExchangeCodeForAccessToken" />
diff --git a/sp-oidc-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/initiator/oidc/oidc-beans.xml b/sp-oidc-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/initiator/oidc/oidc-beans.xml
index 0046940..326ac48 100644
--- a/sp-oidc-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/initiator/oidc/oidc-beans.xml
+++ b/sp-oidc-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/initiator/oidc/oidc-beans.xml
@@ -22,10 +22,10 @@
p:requireDiscoveryURL="false"
p:requireRelyingPartyId="true" />
- <!-- TODO Copied over -->
+ <!-- Prepare the OIDC Peer Entity with the relying party ID (the OP identifier)-->
<bean id="PrepareInboundMessageContext"
class="net.shibboleth.sp.oidc.profile.impl.PrepareOIDCInboundMessageContext" scope="prototype"
- p:identifierLookupStrategy-ref="shibboleth.RelyingPartyIdLookup.Simple">
+ p:relyingPartyLookupStrategy-ref="shibboleth.RelyingPartyIdLookup.Simple">
</bean>
<bean id="ProviderMetadataLookup" parent="WebFlowInboundMessageHandlerAdaptor">
@@ -59,7 +59,8 @@
scope="prototype" />
<bean id="InitializeOAuth2ClientContext" scope="prototype"
- class="net.shibboleth.sp.oidc.profile.impl.InitializeOAuth2ClientContext"/>
+ class="net.shibboleth.sp.oidc.profile.impl.InitializeOAuth2ClientContext"
+ p:issuerLookupStrategy-ref="shibboleth.ClientIdLookup.Simple"/>
<bean id="InitializeAuthorizationRequest"
class="net.shibboleth.sp.oidc.profile.impl.InitializeAuthorizationRequest" scope="prototype"/>
@@ -335,13 +336,26 @@
</bean>
</constructor-arg>
</bean>
-
- <bean id="IssueIssuerCorrelationCookie" class="net.shibboleth.sp.profile.impl.IssueCorrelationCookie" scope="prototype"
+
+ <!-- Issue a cookie with state information that is needs to be recovered to validate the response -->
+ <bean id="IssueAuthnStateCookie" class="net.shibboleth.sp.profile.impl.IssueCorrelationCookie" scope="prototype"
p:cookieManager-ref="shibboleth.sp.RemotedCookieManager"
- p:cookiePrefix="#{'%{sp.correlation.cookiePrefix:__Host-_shibsp__iss_}'.trim()}"
+ p:cookiePrefix="#{'%{sp.correlation.cookiePrefix:__Host-_shibsp__authnstate_}'.trim()}"
p:createOutputObjects="true"
p:errorFatal="%{sp.stateToken.errorsFatal:true}"
- p:requestIDLookupStrategy-ref="shibboleth.RelyingPartyIdLookup.Simple" />
+ p:requestIDLookupStrategy-ref="AuthenticationRequestStateForStorageStrategy" />
+
+ <!-- <bean id="MapAuthenticationStateToStateToken" class="net.shibboleth.sp.oidc.profile.impl.MapStateToStateToken" scope="prototype"
+ p:createOutputObjects="true"
+ p:errorFatal="%{sp.stateToken.errorsFatal:false}" /> -->
+
+ <!-- A strategy for encoding and sealing any state from the authentication request needed for validating the response -->
+ <bean id="AuthenticationRequestStateForStorageStrategy" class="net.shibboleth.sp.oidc.profile.impl.AuthenticationRequestStateForStorageStrategy"
+ p:authenticationAuthorityLookupStrategy-ref="shibboleth.RelyingPartyIdLookup.Simple"
+ p:authenticationRequestLookupStrategy-ref="shibboleth.AuthenticationRequestLookup.FromOutbound"
+ p:dataSealer-ref="#{'%{sp.dataSealer:shibboleth.DataSealer}'.trim()}"
+ p:sealStatePredicate="#{getObject('%{sp.oidc.sealAuthenticationStatePredicate:}') ?: ((%{sp.oidc.sealAuthenticationState:true}) ? getObject('shibboleth.Conditions.TRUE') : getObject('shibboleth.Conditions.FALSE'))}"
+ p:objectMapper="#{getObject('%{sp.oidc.jsonObjectMapper:}') ?: getObject('shibboleth.JSONObjectMapper')}"/>
<bean id="EncodeMessage" class="net.shibboleth.sp.profile.impl.EncodeMessage" scope="prototype"
p:createOutputObjects="true"
diff --git a/sp-oidc-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/initiator/oidc/oidc-flow.xml b/sp-oidc-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/initiator/oidc/oidc-flow.xml
index 6571ccd..7e75a35 100644
--- a/sp-oidc-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/initiator/oidc/oidc-flow.xml
+++ b/sp-oidc-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/initiator/oidc/oidc-flow.xml
@@ -56,7 +56,8 @@
<action-state id="BuildOutboundMessage">
<evaluate expression="HandleOutboundMessage" />
<evaluate expression="IssueNonceCorrelationCookie" />
- <evaluate expression="IssueIssuerCorrelationCookie" />
+ <evaluate expression="IssueAuthnStateCookie" />
+ <!-- <evaluate expression="MapAuthenticationStateToStateToken"/> -->
<evaluate expression="EncodeMessage" />
<evaluate expression="'proceed'" />
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 a3572ff..369b7fc 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
@@ -61,10 +61,24 @@
c:outputType="#{T(com.nimbusds.openid.connect.sdk.op.OIDCProviderMetadata)}"
c:expression="#input.getProviderInformation()" />
- <bean id="shibboleth.ChildLookup.OAuth2ClientContext"
+ <bean id="shibboleth.ChildLookup.AuthnRequestStateDataContext"
+ class="org.opensaml.messaging.context.navigate.ChildContextLookup"
+ c:type="#{ T(net.shibboleth.sp.oidc.context.AuthnRequestStateDataContext) }" />
+
+ <bean id="shibboleth.ChildLookup.OAuth2ClientContext"
class="org.opensaml.messaging.context.navigate.ChildContextLookup"
c:type="#{ T(net.shibboleth.oidc.profile.context.OAuth2ClientContext) }" />
+
+ <bean id="shibboleth.AuthenticationRequestLookup.FromOutbound"
+ class="net.shibboleth.sp.oidc.context.navigate.AuthenticationRequestFromOutboundLookupStrategy" />
+ <!-- Add BiFunction for use with validators that finds the clientId from the sp.issuer -->
+ <bean id="shibboleth.ClientIdLookup.BiFunction" class="net.shibboleth.sp.oidc.context.navigate.ClientIdBiFunctionLookupStrategy">
+ <constructor-arg ref="shibboleth.ClientIdLookup.Simple"/>
+ </bean>
+
+ <!-- An alias to make it clear the clientId is coming from the issuer of the request, lookup -->
+ <alias alias="shibboleth.ClientIdLookup.Simple" name="shibboleth.IssuerLookup.Simple"/>
<bean id="shibboleth.ChildLookup.OIDCProviderMetadataContextFromPeerContext" parent="shibboleth.Functions.Compose">
<constructor-arg name="g">
@@ -102,7 +116,7 @@
<bean id="shibboleth.ChildLookupOrCreate.OAuth2ClientAuthenticationContextFromOIDCPeer" parent="shibboleth.Functions.Compose">
<constructor-arg name="g">
- <ref bean="shibboleth.ChildLookup.OAuth2ClientAuthenticationContext" />
+ <ref bean="shibboleth.ChildLookupOrCreate.OAuth2ClientAuthenticationContext" />
</constructor-arg>
<constructor-arg name="f">
<ref bean="shibboleth.ChildLookup.OIDCPeerEntityContext" />
@@ -153,5 +167,15 @@
<ref bean="shibboleth.ChildLookup.OIDCPeerEntityFromOutbound" />
</constructor-arg>
</bean>
+
+ <bean id="shibboleth.ChildLookup.AuthenticationRequestStateDataFromInbound"
+ parent="shibboleth.Functions.Compose">
+ <constructor-arg name="g">
+ <ref bean="shibboleth.ChildLookup.AuthnRequestStateDataContext" />
+ </constructor-arg>
+ <constructor-arg name="f">
+ <ref bean="shibboleth.MessageContextLookup.Inbound" />
+ </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 82b002b..10868ba 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
@@ -17,13 +17,13 @@
<!-- Import parent beans for metadata-driven configuration approach. -->
<!-- <import resource="relying-party-mddriven.xml" /> -->
- <!--
- Default Profile Configurations, also usable as parent beans in overrides.
-
- Each profile shows up twice, once as a basic bean and again as a child bean that adds
- a suite of metadata-driven lookup strategies pre-injected.
- -->
-
+ <!-- Auto-wired protocol service support bean for use by parent plugin. -->
+ <bean class="net.shibboleth.sp.BasicProtocolSupportService"
+ p:id="OIDC"
+ p:order="%{sp.oidc.relativeOrder:1}"
+ p:sessionInitiators="oidc"
+ p:tokenConsumers="#{{'oidc/code/query', 'oidc/code/post'}}" />
+
<!-- Credential bean factories -->
<bean id="shibboleth.oidc.ClientSecretCredential" abstract="true"
class="net.shibboleth.oidc.security.credential.BasicClientSecretCredentialFactoryBean" />
diff --git a/sp-oidc-conf-impl/src/main/resources/net/shibboleth/idp/module/conf/sp/oidc.properties b/sp-oidc-conf-impl/src/main/resources/net/shibboleth/idp/module/conf/sp/oidc.properties
index ec57c50..72e7f5d 100644
--- a/sp-oidc-conf-impl/src/main/resources/net/shibboleth/idp/module/conf/sp/oidc.properties
+++ b/sp-oidc-conf-impl/src/main/resources/net/shibboleth/idp/module/conf/sp/oidc.properties
@@ -22,4 +22,13 @@ sp.oidc.signing.rsa.enc.key = %{idp.home}/credentials/sp/sp-encryption-rsa.jwk
## TEST ENC FOR NOW from profile config
#idp.oidc.requestobject.encrypted = true
+## TODO check these are sensibly named
+#sp.oidc.jwt.verifier.clockSkew =
+#sp.oidc.idtoken.jwt.verifier.clockSkew:PT60S
+#sp.oidc.idtoken.jwt.verifier.authnLifetime:PT60S
+#sp.oidc.idtoken.jwt.verifier.authnRequestClockSkew:PT0S
+
+# Each protocol plugin will contain an order priority controlling
+# relative order of use when handling requests, lower is "earlier".
+#sp.oidc.relativeOrder = 1
diff --git a/sp-oidc-conf-impl/src/test/java/net/shibboleth/sp/oidc/flows/OIDCSessionInitiatorFlowTest.java b/sp-oidc-conf-impl/src/test/java/net/shibboleth/sp/oidc/flows/OIDCSessionInitiatorFlowTest.java
index 91f4fc0..3ba7456 100644
--- a/sp-oidc-conf-impl/src/test/java/net/shibboleth/sp/oidc/flows/OIDCSessionInitiatorFlowTest.java
+++ b/sp-oidc-conf-impl/src/test/java/net/shibboleth/sp/oidc/flows/OIDCSessionInitiatorFlowTest.java
@@ -1,3 +1,4 @@
+
/*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
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 85eb332..c20c156 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
@@ -23,9 +23,11 @@ import java.net.URI;
import java.net.URISyntaxException;
import java.nio.charset.StandardCharsets;
import java.text.ParseException;
+import java.time.Duration;
import java.time.Instant;
import java.util.Date;
import java.util.HashSet;
+import java.util.Map;
import java.util.Set;
import javax.annotation.Nonnull;
@@ -142,138 +144,360 @@ public class OIDCTokenConsumerFlowTest extends AbstractSPFlowTest {
if (httpClient == null) {
fail("Mocked Http Client could not be found");
}
+
}
/**
- * Test successful flow.
+ * Test successful flow with a signed id_token and a plain user info response.
*
- * @throws IOException
+ * @throws IOException on error
*/
@Test
public void testSuccess_SignedIDToken_PlainUserInfo() throws Exception {
- mockOIDCEndpoints(constructSuccessfulTokenResponseSigned(Instant.now().plusSeconds(3600), Instant.now()),
+ mockOIDCEndpoints(constructSuccessfulTokenResponseSigned(Instant.now().plusSeconds(3600), Instant.now(), null, null),
constructJSONUserInfoResponse());
final AuthenticationSuccessResponse response =
- buildOIDCAuthorizationCodeResponse(TestConstants.RESPONSE_URL, ResponseMode.QUERY, TestConstants.STATE_STRING);
+ buildOIDCAuthorizationCodeResponse(TestConstants.RESPONSE_URL, ResponseMode.QUERY,
+ TestConstants.STATE_STRING);
final DDF input = buildRemotedQueryStringResponse(response);
// Add cookies
- input.addmember("http.headers.Cookie")
- .unsafe_string(TestConstants.COOKIE_HEADER.getBytes("UTF-8"));
+ input.addmember("http.headers.Cookie").unsafe_string(TestConstants.buildCookieHeader(
+ TestConstants.AUTHENTICATION_STATE,
+ TestConstants.APPLICATION_ID,
+ TestConstants.AUTHENTICATION_NONCE,
+ TestConstants.TARGET_URL,
+ TestConstants.buildAuthenticationState(null, false)));
- setApplicationRequest("test-oidc-application-with-ro", input);
+ setApplicationRequest(TestConstants.APPLICATION_ID, 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());
+ System.out.println("test output: " + output.toString());
validateOutputMessage(result, CollectionSupport.setOf("sub","mail","displayName","eduPersonScopedAffiliation"),
TestConstants.RESOURCE_URL);
}
/**
- * Test successful flow.
+ * Test successful flow with a signed id_token and a plain user info response.
+ *
+ * @throws IOException on error
+ */
+ @Test
+ public void testSuccess_SignedIDToken_PlainUserInfo_WithMaxAge() throws Exception {
+
+ mockOIDCEndpoints(constructSuccessfulTokenResponseSigned(Instant.now().plusSeconds(3600), Instant.now(), null,
+ Map.of("auth_time",Date.from(Instant.now()))), constructJSONUserInfoResponse());
+
+ final AuthenticationSuccessResponse response =
+ buildOIDCAuthorizationCodeResponse(TestConstants.RESPONSE_URL, ResponseMode.QUERY,
+ TestConstants.STATE_STRING);
+ final DDF input = buildRemotedQueryStringResponse(response);
+
+ // Add cookies
+ input.addmember("http.headers.Cookie").unsafe_string(TestConstants.buildCookieHeader(
+ TestConstants.AUTHENTICATION_STATE,
+ TestConstants.APPLICATION_ID,
+ TestConstants.AUTHENTICATION_NONCE,
+ TestConstants.TARGET_URL,
+ TestConstants.buildAuthenticationState(Duration.ofMinutes(1), true)));
+
+ setApplicationRequest(TestConstants.APPLICATION_ID, 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("test output: " + output.toString());
+ validateOutputMessage(result, CollectionSupport.setOf("sub","mail","displayName","eduPersonScopedAffiliation"),
+ TestConstants.RESOURCE_URL);
+ }
+
+ /**
+ * Test a failure when max_age was sent in the authentication request but auth_time is missing in the id_token.
+ *
+ * @throws IOException on error
+ */
+ @Test
+ public void testFail_SignedIDToken_PlainUserInfo_WithMaxAge_ButNoAuthTime() throws Exception {
+
+ mockOIDCEndpoints(constructSuccessfulTokenResponseSigned(Instant.now().plusSeconds(3600), Instant.now(), null,
+ null), constructJSONUserInfoResponse());
+
+ final AuthenticationSuccessResponse response =
+ buildOIDCAuthorizationCodeResponse(TestConstants.RESPONSE_URL, ResponseMode.QUERY, TestConstants.STATE_STRING);
+ final DDF input = buildRemotedQueryStringResponse(response);
+
+ // Add cookies
+ input.addmember("http.headers.Cookie").unsafe_string(TestConstants.buildCookieHeader(
+ TestConstants.AUTHENTICATION_STATE,
+ TestConstants.APPLICATION_ID,
+ TestConstants.AUTHENTICATION_NONCE,
+ TestConstants.TARGET_URL,
+ TestConstants.buildAuthenticationState(Duration.ofMinutes(1), true)));
+
+ setApplicationRequest(TestConstants.APPLICATION_ID, input);
+
+ final FlowExecutionResult result = flowExecutor.launchExecution(TestConstants.FLOW_ID, null, externalContext);
+ assertFlowExecutionResult(result, TestConstants.FLOW_ID);
+ assertFlowExecutionOutcome(result.getOutcome());
+ assertOutputMessageEvent(result, EventIds.MESSAGE_PROC_ERROR);
+ }
+
+ /**
+ * Test successful flow with a signed and encrypted id_token and a plain user info response.
*
- * @throws IOException
+ * @throws IOException on error
*/
@Test
public void testSuccess_SignedEncryptedIDToken_PlainUserInfo() throws Exception {
- mockOIDCEndpoints(constructSuccessfulTokenResponseSignedAndEncrypted(Instant.now().plusSeconds(3600), Instant.now()),
- constructJSONUserInfoResponse());
+ mockOIDCEndpoints(constructSuccessfulTokenResponseSignedAndEncrypted(Instant.now().plusSeconds(3600),
+ Instant.now(), null, null), constructJSONUserInfoResponse());
final AuthenticationSuccessResponse response =
buildOIDCAuthorizationCodeResponse(TestConstants.RESPONSE_URL, ResponseMode.QUERY, TestConstants.STATE_STRING);
final DDF input = buildRemotedQueryStringResponse(response);
// Add cookies
- input.addmember("http.headers.Cookie")
- .unsafe_string(TestConstants.COOKIE_HEADER.getBytes("UTF-8"));
+ input.addmember("http.headers.Cookie").unsafe_string(TestConstants.buildCookieHeader(
+ TestConstants.AUTHENTICATION_STATE,
+ TestConstants.APPLICATION_ID,
+ TestConstants.AUTHENTICATION_NONCE,
+ TestConstants.TARGET_URL,
+ TestConstants.buildAuthenticationState(null, false)));
- setApplicationRequest("test-oidc-application-with-ro", input);
+ setApplicationRequest(TestConstants.APPLICATION_ID, 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());
+ System.out.println("test output: " + output.toString());
validateOutputMessage(result, CollectionSupport.setOf("sub","mail","displayName","eduPersonScopedAffiliation"),
TestConstants.RESOURCE_URL);
}
/**
- * Test successful flow.
+ * Test successful flow with a signed id_token and a JWT user info response.
*
- * @throws IOException
+ * @throws IOException on error
*/
@Test
public void testSuccess_SignedIDToken_JWTUserInfo() throws Exception {
- mockOIDCEndpoints(constructSuccessfulTokenResponseSigned(Instant.now().plusSeconds(3600), Instant.now()),
- constructJWTUserInfoResponseSigned());
+ mockOIDCEndpoints(constructSuccessfulTokenResponseSigned(Instant.now().plusSeconds(3600), Instant.now(),
+ null, null), constructJWTUserInfoResponseSigned());
+
+ final AuthenticationSuccessResponse response =
+ buildOIDCAuthorizationCodeResponse(TestConstants.RESPONSE_URL, ResponseMode.QUERY, TestConstants.STATE_STRING);
+ final DDF input = buildRemotedQueryStringResponse(response);
+
+ // Add cookies
+ input.addmember("http.headers.Cookie").unsafe_string(TestConstants.buildCookieHeader(
+ TestConstants.AUTHENTICATION_STATE,
+ TestConstants.APPLICATION_ID,
+ TestConstants.AUTHENTICATION_NONCE,
+ TestConstants.TARGET_URL,
+ TestConstants.buildAuthenticationState(null, false)));
+
+ setApplicationRequest(TestConstants.APPLICATION_ID, 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("test output: " + output.toString());
+ validateOutputMessage(result, CollectionSupport.setOf("sub","mail","displayName","eduPersonScopedAffiliation"),
+ TestConstants.RESOURCE_URL);
+ }
+
+ /**
+ * Test successful flow, where id_token contains an AZP claim.
+ *
+ * @throws IOException on error
+ */
+ @Test
+ public void testSuccess_SignedIDTokenWithAZP_JWTUserInfo() throws Exception {
+
+ mockOIDCEndpoints(constructSuccessfulTokenResponseSigned(Instant.now().plusSeconds(3600), Instant.now(),
+ null, Map.of("azp", TestConstants.CLIENT_ID)), constructJWTUserInfoResponseSigned());
final AuthenticationSuccessResponse response =
buildOIDCAuthorizationCodeResponse(TestConstants.RESPONSE_URL, ResponseMode.QUERY, TestConstants.STATE_STRING);
final DDF input = buildRemotedQueryStringResponse(response);
// Add cookies
- input.addmember("http.headers.Cookie").unsafe_string(TestConstants.COOKIE_HEADER.getBytes("UTF-8"));
+ input.addmember("http.headers.Cookie").unsafe_string(TestConstants.buildCookieHeader(
+ TestConstants.AUTHENTICATION_STATE,
+ TestConstants.APPLICATION_ID,
+ TestConstants.AUTHENTICATION_NONCE,
+ TestConstants.TARGET_URL,
+ TestConstants.buildAuthenticationState(null, false)));
- setApplicationRequest("test-oidc-application-with-ro", input);
+ setApplicationRequest(TestConstants.APPLICATION_ID, 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());
+ System.out.println("test output: " + output.toString());
validateOutputMessage(result, CollectionSupport.setOf("sub","mail","displayName","eduPersonScopedAffiliation"),
TestConstants.RESOURCE_URL);
}
/**
- * Test bad state.
+ * Test a failure if the id_token contains the wrong AZP value (not the client_id).
*
- * @throws IOException
+ * @throws IOException on error
*/
@Test
- public void testFail_BadState() throws IOException {
+ public void testFail_IDToken_AZPMistmatch() throws Exception {
+ mockOIDCEndpoints(constructSuccessfulTokenResponseSigned(Instant.now().plusSeconds(3600), Instant.now(),
+ null, Map.of("azp", "bad-azp")), constructJWTUserInfoResponseSigned());
+
+ final AuthenticationSuccessResponse response =
+ buildOIDCAuthorizationCodeResponse(TestConstants.RESPONSE_URL, ResponseMode.QUERY, TestConstants.STATE_STRING);
+ final DDF input = buildRemotedQueryStringResponse(response);
+
+ // Add cookies
+ input.addmember("http.headers.Cookie").unsafe_string(TestConstants.buildCookieHeader(
+ TestConstants.AUTHENTICATION_STATE,
+ TestConstants.APPLICATION_ID,
+ TestConstants.AUTHENTICATION_NONCE,
+ TestConstants.TARGET_URL,
+ TestConstants.buildAuthenticationState(null, false)));
+
+ setApplicationRequest(TestConstants.APPLICATION_ID, input);
+
+ final FlowExecutionResult result = flowExecutor.launchExecution(TestConstants.FLOW_ID, null, externalContext);
+ assertFlowExecutionResult(result, TestConstants.FLOW_ID);
+ assertFlowExecutionOutcome(result.getOutcome());
+ assertOutputMessageEvent(result, EventIds.MESSAGE_PROC_ERROR);
+ }
+
+ /**
+ * Test a failure, nonce mismatch in the id_token.
+ *
+ * @throws Exception on error
+ */
+ @Test
+ public void testFail_IDToken_NonceMismatch() throws Exception {
+
+ mockOIDCEndpoints(constructSuccessfulTokenResponseSigned(Instant.now().plusSeconds(3600),
+ Instant.now(), "bad-idtoken-nonce", null), constructJWTUserInfoResponseSigned());
+
+ final AuthenticationSuccessResponse response =
+ buildOIDCAuthorizationCodeResponse(TestConstants.RESPONSE_URL, ResponseMode.QUERY, TestConstants.STATE_STRING);
+ final DDF input = buildRemotedQueryStringResponse(response);
+
+ // Add cookies
+ input.addmember("http.headers.Cookie").unsafe_string(TestConstants.buildCookieHeader(
+ TestConstants.AUTHENTICATION_STATE,
+ TestConstants.APPLICATION_ID,
+ TestConstants.AUTHENTICATION_NONCE,
+ TestConstants.TARGET_URL,
+ TestConstants.buildAuthenticationState(null, false)));
+
+ setApplicationRequest(TestConstants.APPLICATION_ID, input);
+
+ final FlowExecutionResult result = flowExecutor.launchExecution(TestConstants.FLOW_ID, null, externalContext);
+ assertFlowExecutionResult(result, TestConstants.FLOW_ID);
+ assertFlowExecutionOutcome(result.getOutcome());
+ assertOutputMessageEvent(result, EventIds.MESSAGE_PROC_ERROR);
+ }
+
+ /**
+ * Test a failure, nonce mismatch in the id_token.
+ *
+ * @throws Exception on error
+ */
+ @Test
+ public void testSuccess_IDToken_WithNonce() throws Exception {
+
+ mockOIDCEndpoints(constructSuccessfulTokenResponseSigned(Instant.now().plusSeconds(3600),
+ Instant.now(), TestConstants.ID_TOKEN_NONCE, null), constructJWTUserInfoResponseSigned());
+
+ final AuthenticationSuccessResponse response =
+ buildOIDCAuthorizationCodeResponse(TestConstants.RESPONSE_URL, ResponseMode.QUERY, TestConstants.STATE_STRING);
+ final DDF input = buildRemotedQueryStringResponse(response);
+
+ // Add cookies
+ input.addmember("http.headers.Cookie").unsafe_string(TestConstants.buildCookieHeader(
+ TestConstants.AUTHENTICATION_STATE,
+ TestConstants.APPLICATION_ID,
+ TestConstants.AUTHENTICATION_NONCE,
+ TestConstants.TARGET_URL,
+ TestConstants.buildAuthenticationState(null, false)));
+
+ setApplicationRequest(TestConstants.APPLICATION_ID, 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("test output: " + output.toString());
+ validateOutputMessage(result, CollectionSupport.setOf("sub","mail","displayName","eduPersonScopedAffiliation"),
+ TestConstants.RESOURCE_URL);
+ }
+
+ /**
+ * Test failure, a bad nonce in the response state.
+ *
+ * @throws IOException on error
+ */
+ @Test
+ public void testFail_BadCorrelationNonceState() throws Exception {
final AuthenticationSuccessResponse response =
buildOIDCAuthorizationCodeResponse(TestConstants.RESPONSE_URL, ResponseMode.QUERY, TestConstants.STATE_STRING);
final DDF input = buildRemotedQueryStringResponse(response);
// Add cookies
- input.addmember("http.headers.Cookie")
- .unsafe_string(TestConstants.COOKIE_HEADER_BAD_STATE.getBytes("UTF-8"));
+ input.addmember("http.headers.Cookie").unsafe_string(TestConstants.buildCookieHeader(
+ TestConstants.AUTHENTICATION_STATE,
+ TestConstants.APPLICATION_ID,
+ "bad-nonce", //this is a bad nonce
+ TestConstants.TARGET_URL,
+ TestConstants.buildAuthenticationState(null, false)));
- setApplicationRequest("test-oidc-application-with-ro", input);
+ setApplicationRequest(TestConstants.APPLICATION_ID, input);
final FlowExecutionResult result = flowExecutor.launchExecution(TestConstants.FLOW_ID, null, externalContext);
assertFlowExecutionResult(result, TestConstants.FLOW_ID);
assertFlowExecutionOutcome(result.getOutcome());
assertOutputMessageEvent(result, EventIds.MESSAGE_PROC_ERROR);
}
+
/**
* Test successful flow.
*
- * @throws IOException
+ * @throws IOException on error
*/
@Test
- public void testFail_ErrorResponse() throws IOException {
+ public void testFail_ErrorResponse() throws Exception {
final DDF input = buildRemotedQueryStringResponse(buildErrorResponse(TestConstants.STATE_STRING));
// Add cookies
- input.addmember("http.headers.Cookie")
- .unsafe_string(TestConstants.COOKIE_HEADER_BAD_STATE.getBytes("UTF-8"));
+ input.addmember("http.headers.Cookie").unsafe_string(TestConstants.buildCookieHeader(
+ TestConstants.AUTHENTICATION_STATE,
+ TestConstants.APPLICATION_ID,
+ TestConstants.AUTHENTICATION_NONCE,
+ TestConstants.TARGET_URL,
+ TestConstants.buildAuthenticationState(null, false)));
- setApplicationRequest("test-oidc-application-with-ro", input);
+ setApplicationRequest(TestConstants.APPLICATION_ID, input);
final FlowExecutionResult result = flowExecutor.launchExecution(TestConstants.FLOW_ID, null, externalContext);
assertFlowExecutionResult(result, TestConstants.FLOW_ID);
@@ -287,20 +511,35 @@ public class OIDCTokenConsumerFlowTest extends AbstractSPFlowTest {
*
* @param expiry expiry time
* @param issuedAt issue time
+ * @param nonce the nonce
* @return the tokens
* @throws ParseException
* @throws JOSEException
*/
private OIDCTokenResponse constructSuccessfulTokenResponseSigned(
- @Nonnull final Instant expiry, @Nonnull final Instant issuedAt) throws JOSEException, ParseException {
+ @Nonnull final Instant expiry, @Nonnull final Instant issuedAt, @Nullable final String nonce,
+ @Nullable final Map<String, Object> additionalClaims)
+ throws JOSEException, ParseException {
- final JWTClaimsSet claimsSet = new JWTClaimsSet.Builder()
- .subject("jdoe")
- .issuer("https://op.example.org")
- .audience("mock-client-id")
- .expirationTime(Date.from(expiry))
- .issueTime(Date.from(issuedAt))
- .build();
+ final JWTClaimsSet.Builder claimsSetBuilder = new JWTClaimsSet.Builder()
+ .subject("jdoe")
+ .issuer(TestConstants.ISSUER)
+ .audience(TestConstants.CLIENT_ID)
+ .expirationTime(Date.from(expiry))
+ .issueTime(Date.from(issuedAt));
+
+ if (nonce != null) {
+ claimsSetBuilder.claim("nonce", nonce);
+ }
+ if (additionalClaims != null) {
+ additionalClaims.entrySet()
+ .stream()
+ .forEach(entry -> claimsSetBuilder.claim(entry.getKey(), entry.getValue()));
+ }
+
+ final JWTClaimsSet claimsSet = claimsSetBuilder.build();
+
+
final JWT signedIdToken =
TestTokenHelper.createJWT(claimsSet, JWSAlgorithm.RS256, null, null, opSigningCredential, null);
@@ -314,20 +553,33 @@ public class OIDCTokenConsumerFlowTest extends AbstractSPFlowTest {
*
* @param expiry expiry time
* @param issuedAt issue time
+ * @param nonce the nonce
* @return the tokens
* @throws ParseException
* @throws JOSEException
*/
private OIDCTokenResponse constructSuccessfulTokenResponseSignedAndEncrypted(
- @Nonnull final Instant expiry, @Nonnull final Instant issuedAt) throws JOSEException, ParseException {
+ @Nonnull final Instant expiry, @Nonnull final Instant issuedAt, @Nullable final String nonce,
+ @Nullable final Map<String, Object> additionalClaims)
+ throws JOSEException, ParseException {
- final JWTClaimsSet claimsSet = new JWTClaimsSet.Builder()
+ final JWTClaimsSet.Builder claimsSetBuilder = new JWTClaimsSet.Builder()
.subject("jdoe")
- .issuer("https://op.example.org")
- .audience("mock-client-id")
+ .issuer(TestConstants.ISSUER)
+ .audience(TestConstants.CLIENT_ID)
.expirationTime(Date.from(expiry))
- .issueTime(Date.from(issuedAt))
- .build();
+ .issueTime(Date.from(issuedAt));
+
+ if (nonce != null) {
+ claimsSetBuilder.claim("nonce", nonce);
+ }
+ if (additionalClaims != null) {
+ additionalClaims.entrySet()
+ .stream()
+ .forEach(entry -> claimsSetBuilder.claim(entry.getKey(), entry.getValue()));
+ }
+ final JWTClaimsSet claimsSet = claimsSetBuilder.build();
+
final JWT encryptedIdToken =
TestTokenHelper.createJWT(claimsSet, JWSAlgorithm.RS256, JWEAlgorithm.RSA_OAEP_256,
EncryptionMethod.A128CBC_HS256, opSigningCredential, rpEncryptionCredential);
@@ -359,7 +611,7 @@ public class OIDCTokenConsumerFlowTest extends AbstractSPFlowTest {
private UserInfoSuccessResponse constructJWTUserInfoResponseSigned() throws JOSEException, ParseException {
final JWTClaimsSet claimsSet = new JWTClaimsSet.Builder()
.subject("jdoe")
- .issuer("https://op.example.org")
+ .issuer(TestConstants.ISSUER)
.audience("mock-client-id")
.claim("name", "John Doe").build();
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
index 3dd5dd7..aaa9845 100644
--- 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
@@ -14,8 +14,28 @@
package net.shibboleth.sp.oidc.flows;
+import static org.testng.Assert.fail;
+
+import java.net.URLEncoder;
+import java.nio.charset.StandardCharsets;
+import java.time.Duration;
+import java.time.Instant;
+
import javax.annotation.Nonnull;
+import com.fasterxml.jackson.annotation.JsonInclude.Include;
+import com.fasterxml.jackson.core.Base64Variants;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.DeserializationFeature;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.json.JsonMapper;
+import com.fasterxml.jackson.datatype.jdk8.Jdk8Module;
+import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
+
+import net.shibboleth.shared.annotation.constraint.NotEmpty;
+import net.shibboleth.shared.codec.Base64Support;
+import net.shibboleth.sp.oidc.profile.AuthenticationRequestStateData;
+
/** A class to hold constants used for tests.*/
public final class TestConstants {
@@ -23,6 +43,9 @@ public final class TestConstants {
private TestConstants() {
}
+
+ /** The client_id to use, must match the issuer in oidc-test-agents.xml.*/
+ @Nonnull @NotEmpty public static final String CLIENT_ID = "testsp.example.org";
/** Flow ID. */
@Nonnull public static final String FLOW_ID = "sp/token-consumer";
@@ -38,40 +61,91 @@ public final class TestConstants {
/** Resource URL. */
@Nonnull public static final String RESOURCE_URL = "https://sp.example.org/secure";
+
+ /** The nonce as part of the state returned in an authentication response. Not the nonce in an id_token.*/
+ @Nonnull public static final String AUTHENTICATION_NONCE = "f26d922221226cd183729bf13247dbe2";
+
+ /** The nonce in the ID token.*/
+ @Nonnull public static final String ID_TOKEN_NONCE = "bd1b5f211250c57e";
+
+ /** State string used in cookie names. */
+ public static final String AUTHENTICATION_STATE = "1761316967710_1622a5c726da8f7b36e24f19eed82aea";
+
+ /** Application ID used in cookie names. */
+ public static final String APPLICATION_ID = "test-oidc-application-with-ro";
+
+ /** Target URL encoded in cookie. */
+ public static final String TARGET_URL = "https://sp.example.org/secure";
/**
* A returned state String consisting of a state and a nonce:
*
- * <code>
+ * <pre>
* {"state":"1761316967710_1622a5c726da8f7b36e24f19eed82aea",
* "nonce":"f26d922221226cd183729bf13247dbe2"}
- * </code>
+ * </pre>
* */
public static final String STATE_STRING = """
eyJzdGF0ZSI6IjE3NjEzMTY5Njc3MTBfMTYyMmE1YzcyNmRhOGY3YjM2ZTI0ZjE5ZWVkODJhZWEiLCJub25jZSI6ImYyNmQ5MjIyMjEyMjZjZDE4MzcyOWJmMTMyNDdkYmUyIn0
""";
/**
- * Cookie headers representing the state string {@literal STATE_STRING}:
- *
+ * Build cookie header bytes for the given parameters. These take the form of:
+ * <pre>
* __Host-_shibsp_req_{state}={nonce};
- * _Host-shibsp_state__{appId}_{state}={targetURL};
- * __Host-_shibsp_iss_{state}={issuer};
+ * __Host-shibsp_state__{appId}_{state}={targetURL};
+ * __Host-_shibsp_authnstate_{state}={authn_request_json};
+ * </pre>
*
+ * @param state the state used for mapping response to the stored headers
+ * @param appID the application ID
+ * @param nonce the nonce used to protect against CSRF in the request and response OAuth state parameter
+ * not to be confused with the OIDC ID token nonce.
+ * @param targetURL the target URL to redirect to after processing
+ * @param authnRequestStateJSON the authentication request state JSON for recovery of important parameters
+ * @return the cookie header bytes
+ * @throws Exception on error
*/
- 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;
- """;
+ public static byte[] buildCookieHeader(final String state, final String appID, final String nonce,
+ final String targetURL, final String authnRequestStateJSON) throws Exception {
+
+
+ final String targetUrlB64 = Base64Support.encodeURLSafe(targetURL.getBytes(StandardCharsets.UTF_8));
+ final String authnJsonEnc = URLEncoder.encode(authnRequestStateJSON, StandardCharsets.UTF_8);
+
+ // Compose the header with semicolons and spacing as in the example
+ final StringBuilder sb = new StringBuilder();
+ sb.append("__Host-_shibsp_req_").append(state).append('=').append(nonce).append(";\n");
+ sb.append("__Host-shibsp_state__").append(appID).append('_').append(state)
+ .append('=').append(targetUrlB64).append("; \n");
+ sb.append("__Host-_shibsp_authnstate_").append(state).append('=').append(authnJsonEnc).append(";\n");
+ System.out.println(sb.toString());
+ return sb.toString().getBytes("UTF-8");
+
+ }
- /**
- * Cookie headers representing a bad state string where the nonce is incorrect, as recovered from
- * the __Host-_shibsp_req cookie.
- */
- public static final String COOKIE_HEADER_BAD_STATE = """
- __Host-_shibsp_req_1761316967710_1622a5c726da8f7b36e24f19eed82aea=222cbf13247dbe2;
- _Host-shibsp_state__test-oidc-application-with-ro_1761316967710_1622a5c726da8f7b36e24f19eed82aea=aHR0cHM6Ly9zcC5leGFtcGxlLm9yZy9zZWN1cmU;
- __Host-_shibsp_iss_1761316967710_1622a5c726da8f7b36e24f19eed82aea=https%3A%2F%2Fop.example.org;
- """;
+ public static String buildAuthenticationState(final Duration maxAge, final boolean authTimeRequired) {
+ final AuthenticationRequestStateData state = new AuthenticationRequestStateData()
+ .setClientId(APPLICATION_ID)
+ .setNonce(ID_TOKEN_NONCE)
+ .setAuthenticationAuthority(ISSUER)
+ .setMaxAge(maxAge)
+ .setAuthTimeRequired(authTimeRequired)
+ .setAuthnRequestTime(Instant.now());
+
+ final ObjectMapper jsonMapper = JsonMapper.builder()
+ .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
+ .serializationInclusion(Include.NON_ABSENT)
+ .defaultBase64Variant(Base64Variants.MODIFIED_FOR_URL)
+ .addModule(new Jdk8Module())
+ .addModule(new JavaTimeModule())
+ .build();
+
+ try {
+ return jsonMapper.writeValueAsString(state);
+ } catch (final JsonProcessingException e) {
+ fail(e.getMessage());
+ }
+ return "";
+ }
}
diff --git a/sp-oidc-conf-impl/src/test/java/net/shibboleth/sp/oidc/flows/TestSPOIDCEnvironmentApplicationContextInitializer.java b/sp-oidc-conf-impl/src/test/java/net/shibboleth/sp/oidc/flows/TestSPOIDCEnvironmentApplicationContextInitializer.java
index b595e57..c7b6512 100644
--- a/sp-oidc-conf-impl/src/test/java/net/shibboleth/sp/oidc/flows/TestSPOIDCEnvironmentApplicationContextInitializer.java
+++ b/sp-oidc-conf-impl/src/test/java/net/shibboleth/sp/oidc/flows/TestSPOIDCEnvironmentApplicationContextInitializer.java
@@ -47,6 +47,8 @@ public class TestSPOIDCEnvironmentApplicationContextInitializer extends TestSPEn
mock.setProperty("sp.agent.authn.method", "basic");
// Resolve attributes, to test attribute resolution
mock.setProperty("sp.oidc.resolveAttributes", "true");
+ // Turn off the sealing of authentication request state for tests
+ mock.setProperty("sp.oidc.sealAuthenticationState", "false");
mock.setProperty("idp.additionalProperties",
"/conf/ldap.properties, /conf/saml-nameid.properties, /conf/services.properties, /conf/admin/admin.properties, /conf/authn/authn.properties, /conf/c14n/subject-c14n.properties, /credentials/secrets.properties, /conf/sp/sp.properties, /conf/sp/oidc.properties, /conf/sp/oidc-test.properties");
applicationContext.getEnvironment().getPropertySources().addFirst(mock);
diff --git a/sp-oidc-conf-impl/src/test/resources/net/shibboleth/idp/module/conf/sp/sp.properties b/sp-oidc-conf-impl/src/test/resources/net/shibboleth/idp/module/conf/sp/sp.properties
index 83ae124..0859911 100644
--- a/sp-oidc-conf-impl/src/test/resources/net/shibboleth/idp/module/conf/sp/sp.properties
+++ b/sp-oidc-conf-impl/src/test/resources/net/shibboleth/idp/module/conf/sp/sp.properties
@@ -10,23 +10,6 @@ sp.defaultAuthority = https://op.example.org
#sp.service.agents.failFast = false
sp.service.agents.checkInterval = PT5M
-# Default agent IP address filter
-#sp.agent.authn.allowedAddressRanges = 127.0.0.1/32, ::1/128
-# Set to basic for shared secret authentication
-#sp.agent.authn.method =
-# Set false to globally disable cookie-based authentication by agents
-#sp.agent.authn.cached = true
-#sp.agent.authn.cacheDuration = PT1H
-
-# Set to StorageService to use for remoted storage data if in use.
-#sp.storageService = shibboleth.StorageService
-# Set to DataSealer to use for remoted data encryption.
-#sp.dataSealer = shibboleth.DataSealer
-
-# Default precedence/set of Session Initiator and Token Consumer flows to attempt
-sp.application.sessionInitiators = oidc
-sp.application.tokenConsumers = oidc/code/query, oidc/code/post
-
# General SP cookie properties
#sp.cookie.secure = true
#sp.cookie.httpOnly = true
@@ -39,6 +22,7 @@ sp.application.tokenConsumers = oidc/code/query, oidc/code/post
#sp.cookie.limit = 10
# Default state token management (SAML RelayState, etc.)
+#sp.stateToken.errorsFatal = false
# Set to shibboleth.sp.CookieStateTokenManager to switch to cookie-based mechanism
#sp.stateToken.Manager = shibboleth.sp.StorageStateTokenManager
# Controls storage back-end for storage-based state tokens
@@ -47,8 +31,102 @@ sp.application.tokenConsumers = oidc/code/query, oidc/code/post
#sp.stateToken.cookiePrefix = __Host-shibsp_state_
# Request/response correlation control
-#sp.correlation.cookiePrefix = __Host-_shibsp_req_
+#sp.correlation.cookiePrefix = __Host-shibsp_req_
+
+# POST data preservation controls
+#sp.postData.preservation = false
+#sp.postData.limit = 1048576
+#sp.postData.lifetime = PT5M
+#sp.postData.cookiePrefix = __Host-shibsp_post_
+#sp.postData.StorageService = shibboleth.StorageService
+#sp.postData.errorsFatal = false
# Uncomment/set to define a default IdP discovery service URL or Function
-#sp.discoveryURL = https://test.shibboleth.net/shibboleth-ds/index.html
+#sp.discoveryURL =
#sp.discoveryURLFunction =
+
+# Settings for Hub-mediated session storage
+#sp.session.storageService =
+#sp.session.maxStorageTimeout = P1D
+
+###############################
+# Agent Authentication Settings
+###############################
+
+# Default agent IP address filter
+#sp.agent.authn.allowedAddressRanges = 127.0.0.1/32, ::1/128
+# Set to basic for shared secret authentication
+#sp.agent.authn.method =
+# Set false to globally disable cookie-based authentication by agents
+#sp.agent.authn.cached = true
+#sp.agent.authn.cacheDuration = PT1H
+
+
+# Properties exist for optional "enterprise" agent secret validation. Most of them
+# are for LDAP, replicating the large range of properties available in the IdP "proper"
+# but kept separate for flexibility. They are "chained" to the IdP properties in most
+# cases for convenience but can be overridden if necessary.
+
+#sp.agent.authn.JAAS.loginConfigNames = ShibSPAgentAuth
+#sp.agent.authn.JAAS.loginConfig = %{idp.home}/conf/sp/jaas.config
+
+#sp.agent.authn.Krb5.servicePrincipal =
+#sp.agent.authn.Krb5.keytab =
+
+## Authenticator strategy,
+# either anonSearchAuthenticator, bindSearchAuthenticator, directAuthenticator, adAuthenticator
+#sp.agent.authn.LDAP.authenticator = %{idp.authn.LDAP.authenticator:anonSearchAuthenticator}
+
+## Connection properties ##
+#sp.agent.authn.LDAP.ldapURL = %{idp.authn.LDAP.ldapURL:ldap://localhost:10389}
+#sp.agent.authn.LDAP.useStartTLS = %{idp.authn.LDAP.useStartTLS:true}
+# Time to wait for startTLS responses
+#sp.agent.authn.LDAP.startTLSTimeout = %{idp.authn.LDAP.startTLSTimeout:PT3S}
+# Time to wait for connections to open
+#sp.agent.authn.LDAP.connectTimeout = %{idp.authn.LDAP.connectTimeout:PT3S}
+# Time to wait for operation responses (e.g. search, bind)
+#sp.agent.authn.LDAP.responseTimeout = %{idp.authn.LDAP.responseTimeout:PT3S}
+# Connection strategy to use when multiple URLs are supplied, either ACTIVE_PASSIVE, ROUND_ROBIN, RANDOM
+#sp.agent.authn.LDAP.connectionStrategy = %{idp.authn.LDAP.connectionStrategy:ACTIVE_PASSIVE}
+
+## SSL configuration, either jvmTrust, certificateTrust, or keyStoreTrust
+#sp.agent.authn.LDAP.sslConfig = %{idp.authn.LDAP.sslConfig:certificateTrust}
+## If using certificateTrust above, set to the trusted certificate's path
+#sp.agent.authn.LDAP.trustCertificates = %{idp.authn.LDAP.trustCertificates:%{idp.home}/credentials/ldap-server.crt}
+## If using keyStoreTrust above, set to the truststore path
+#sp.agent.authn.LDAP.trustStore = %{idp.authn.LDAP.trustStore:%{idp.home}/credentials/ldap-server.truststore}
+
+## Return attributes during authentication
+# Setting this property will cause entry resolution to occur as part of authentication
+# Note that this property is not compatible with the adAuthenticator
+#sp.agent.authn.LDAP.returnAttributes = %{idp.authn.LDAP.returnAttributes:1.1}
+
+## DN resolution properties ##
+
+# Search DN resolution, used by anonSearchAuthenticator, bindSearchAuthenticator
+# for AD: CN=Users,DC=example,DC=org
+#sp.agent.authn.LDAP.baseDN = %{idp.authn.LDAP.baseDN:ou=people,dc=example,dc=org}
+#sp.agent.authn.LDAP.subtreeSearch = %{idp.authn.LDAP.subtreeSearch:false}
+#sp.agent.authn.LDAP.userFilter = %{idp.authn.LDAP.userFilter:undefined}
+# bind search configuration
+# for AD: sp.agent.authn.LDAP.bindDN=adminuser at domain.com
+#sp.agent.authn.LDAP.bindDN = %{idp.authn.LDAP.bindDN:uid=myservice,ou=system}
+
+# Format DN resolution, used by directAuthenticator, adAuthenticator
+# for AD use sp.agent.authn.LDAP.dnFormat=%s at domain.com
+#sp.agent.authn.LDAP.dnFormat = %{idp.authn.LDAP.dnFormat:uid=%s,ou=people,dc=example,dc=org}
+
+# pool passivator, either none, bind or anonymousBind
+#sp.agent.authn.LDAP.bindPoolPassivator = %{idp.authn.LDAP.bindPoolPassivator:none}
+
+# LDAP pool configuration, used for both authn and DN resolution
+#sp.agent.authn.pool.LDAP.minSize = %{idp.authn.pool.LDAP.minSize:3}
+#sp.agent.authn.pool.LDAP.maxSize = %{idp.authn.pool.LDAP.maxSize:10}
+#sp.agent.authn.pool.LDAP.validateOnCheckout = %{idp.authn.pool.LDAP.validateOnCheckout:false}
+#sp.agent.authn.pool.LDAP.validatePeriodically = %{idp.authn.pool.LDAP.validatePeriodically:true}
+#sp.agent.authn.pool.LDAP.validatePeriod = %{idp.authn.pool.LDAP.validatePeriod:PT5M}
+#sp.agent.authn.pool.LDAP.validateDN = %{idp.authn.pool.LDAP.validateDN:}
+#sp.agent.authn.pool.LDAP.validateFilter = %{idp.authn.pool.LDAP.validateFilter:(objectClass=*)}
+#sp.agent.authn.pool.LDAP.prunePeriod = %{idp.authn.pool.LDAP.prunePeriod:PT5M}
+#sp.agent.authn.pool.LDAP.idleTime = %{idp.authn.pool.LDAP.idleTime:PT10M}
+#sp.agent.authn.pool.LDAP.blockWaitTime = %{idp.authn.pool.LDAP.blockWaitTime:PT3S}
diff --git a/sp-oidc-impl/pom.xml b/sp-oidc-impl/pom.xml
index 99a0102..bf65051 100644
--- a/sp-oidc-impl/pom.xml
+++ b/sp-oidc-impl/pom.xml
@@ -146,6 +146,27 @@
<groupId>jakarta.servlet</groupId>
<artifactId>jakarta.servlet-api</artifactId>
<scope>provided</scope>
+ </dependency>
+
+ <dependency>
+ <groupId>com.fasterxml.jackson.core</groupId>
+ <artifactId>jackson-databind</artifactId>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>com.fasterxml.jackson.datatype</groupId>
+ <artifactId>jackson-datatype-jdk8</artifactId>
+ <scope>compile</scope>
+ </dependency>
+ <dependency>
+ <groupId>com.fasterxml.jackson.core</groupId>
+ <artifactId>jackson-core</artifactId>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>com.fasterxml.jackson.core</groupId>
+ <artifactId>jackson-annotations</artifactId>
+ <scope>provided</scope>
</dependency>
<!-- Test Dependencies -->
diff --git a/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/AuthTimeRequestedActivationCondition.java b/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/AuthTimeRequestedActivationCondition.java
new file mode 100644
index 0000000..88a6b6e
--- /dev/null
+++ b/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/AuthTimeRequestedActivationCondition.java
@@ -0,0 +1,81 @@
+/*
+ * 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.BiPredicate;
+import java.util.function.Function;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.slf4j.Logger;
+
+import com.nimbusds.jwt.JWTClaimsSet;
+
+import net.shibboleth.shared.annotation.ParameterName;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.primitive.LoggerFactory;
+import net.shibboleth.sp.oidc.context.AuthnRequestStateDataContext;
+
+/**
+ * Has the auth_time claim been requested? This is taken from the authentication request state. A failure is treated
+ * as a request for auth_time to be checked (for a better security posture).
+ */
+public final class AuthTimeRequestedActivationCondition implements BiPredicate<ProfileRequestContext, JWTClaimsSet> {
+
+ /** Class logger. */
+ @Nonnull private final Logger log = LoggerFactory.getLogger(AuthTimeRequestedActivationCondition.class);
+
+ /** Strategy to find the authentication request.*/
+ @Nonnull private final
+ Function<ProfileRequestContext, AuthnRequestStateDataContext> authenticationRequestLookupStrategy;
+
+ /**
+ * Constructor.
+ *
+ * @param strategy strategy to find the authentication request
+ */
+ public AuthTimeRequestedActivationCondition(@ParameterName(name = "authenticationRequestLookupStrategy")
+ @Nonnull final Function<ProfileRequestContext, AuthnRequestStateDataContext> strategy) {
+ authenticationRequestLookupStrategy = Constraint.isNotNull(strategy,
+ "authenticationRequestLookupStrategy can not be null");
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * <p>The claims parameter is ignored.</p>
+ */
+ @Override
+ public boolean test(@Nullable final ProfileRequestContext prc, @Nullable final JWTClaimsSet claims) {
+
+ final AuthnRequestStateDataContext authnRequestCtx = authenticationRequestLookupStrategy.apply(prc);
+ final var localAuthnRequest = authnRequestCtx != null ? authnRequestCtx.getAuthnState() : null;
+ if (authnRequestCtx == null || localAuthnRequest == null) {
+ // Return true, because if we are not sure, we should default to testing the auth_time as a better security
+ // posture
+ log.trace("Authentication request state not found in profile context, is the "
+ + "authenticationRequestLookupStrategy configured correctly? 'auth_time' will now be checked");
+ return true;
+ }
+ final boolean authTimeWasRequested = localAuthnRequest.isAuthTimeRequired();
+ log.trace("Authentication time (auth_time) {} request during authentication",
+ authTimeWasRequested ? "was" : "was not");
+ return authTimeWasRequested;
+
+ }
+
+}
diff --git a/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/AuthenticationRequestStateForStorageStrategy.java b/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/AuthenticationRequestStateForStorageStrategy.java
new file mode 100644
index 0000000..826d115
--- /dev/null
+++ b/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/AuthenticationRequestStateForStorageStrategy.java
@@ -0,0 +1,209 @@
+/*
+ * 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 java.util.function.Predicate;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.slf4j.Logger;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+import net.shibboleth.oidc.profile.core.OIDCAuthenticationRequest;
+import net.shibboleth.shared.annotation.constraint.NonnullAfterInit;
+import net.shibboleth.shared.component.AbstractIdentifiableInitializableComponent;
+import net.shibboleth.shared.component.ComponentInitializationException;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.logic.PredicateSupport;
+import net.shibboleth.shared.primitive.LoggerFactory;
+import net.shibboleth.shared.security.DataSealer;
+import net.shibboleth.shared.security.DataSealerException;
+import net.shibboleth.sp.oidc.profile.AuthenticationRequestStateData;
+
+/**
+ * A {@link Function} that produces a serialized JSON string representing authentication request state that is required
+ * for validating the authentication response. This can be stored in a way that can be recovered later when the
+ * response is received.
+ *
+ * <p>Importantly, if an authentication authority (relying party) is not available, or the authentication request
+ * is not available, null will be returned. This will ultimately prevent the authentication response from being
+ * validated correctly, and the calling action may wish to terminate the authentication request before a response
+ * is made.</p>
+ */
+public class AuthenticationRequestStateForStorageStrategy extends AbstractIdentifiableInitializableComponent
+ implements Function<ProfileRequestContext,String> {
+
+ /** Class logger. */
+ @Nonnull
+ private final Logger log = LoggerFactory.getLogger(AuthenticationRequestStateForStorageStrategy.class);
+
+ /** Strategy to locate authentication authority, that is the issuer of the eventual authentication response. */
+ @NonnullAfterInit private Function<ProfileRequestContext,String> authenticationAuthorityLookupStrategy;
+
+ /** JSON object mapper. */
+ @NonnullAfterInit private ObjectMapper objectMapper;
+
+ /** Strategy to locate the authentication request. */
+ @NonnullAfterInit private Function<ProfileRequestContext,OIDCAuthenticationRequest> authenticationRequestLookupStrategy;
+
+ /** Optional data sealer to use. */
+ @Nullable private DataSealer dataSealer;
+
+ /**
+ * If the dataSealer is provided should it be used to seal the authentication request state? Defaults to true,
+ * that is, if the dataSealer is provided, always seal state.
+ */
+ @Nonnull private Predicate<ProfileRequestContext> sealState;
+
+ /** Constructor.*/
+ public AuthenticationRequestStateForStorageStrategy() {
+ sealState = PredicateSupport.alwaysTrue();
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected void doInitialize() throws ComponentInitializationException {
+ super.doInitialize();
+
+ if (authenticationAuthorityLookupStrategy == null) {
+ throw new ComponentInitializationException("Authentication authority lookup strategy cannot be null");
+ }
+ if (authenticationRequestLookupStrategy == null) {
+ throw new ComponentInitializationException("Authentication request lookup strategy can not be null");
+ }
+ if (objectMapper == null) {
+ throw new ComponentInitializationException("ObjectMapper cannot be null");
+ }
+ }
+
+ /**
+ * Sets {@link DataSealer} to use.
+ *
+ * @param sealer data sealer
+ */
+ public void setDataSealer(@Nullable final DataSealer sealer) {
+ checkSetterPreconditions();
+
+ dataSealer = sealer;
+ }
+
+ /**
+ * Set the predicate to determine whether to seal the state.
+ *
+ * @param predicate the seal state predicate to set.
+ */
+ public void setSealStatePredicate(@Nonnull final Predicate<ProfileRequestContext> predicate) {
+ checkSetterPreconditions();
+
+ sealState = Constraint.isNotNull(predicate, "Seal state predicate can not be null");
+ }
+
+ /**
+ * Set the flag to determine whether to seal the state.
+ *
+ * @param flag the flag to set.
+ */
+ public void setSealState(final boolean flag) {
+ checkSetterPreconditions();
+
+ sealState = flag ? PredicateSupport.alwaysTrue() : PredicateSupport.alwaysFalse();
+ }
+
+ /**
+ * Set the JSON {@link ObjectMapper} to use for serialization.
+ *
+ * @param mapper object mapper
+ */
+ public void setObjectMapper(@Nonnull final ObjectMapper mapper) {
+ checkSetterPreconditions();
+ objectMapper = Constraint.isNotNull(mapper, "ObjectMapper cannot be null");
+ }
+
+ /**
+ * Set the authentication request lookup strategy to use.
+ *
+ * @param strategy the strategy
+ */
+ public void setAuthenticationRequestLookupStrategy(
+ @Nonnull final Function<ProfileRequestContext, OIDCAuthenticationRequest> strategy) {
+ checkSetterPreconditions();
+ authenticationRequestLookupStrategy = Constraint.isNotNull(strategy,
+ "AuthenticationRequestLookupStrategy can not be null");
+ }
+
+ /**
+ * Set the authentication authority lookup strategy to use.
+ *
+ * @param strategy the strategy.
+ */
+ public void setAuthenticationAuthorityLookupStrategy(@Nonnull final Function<ProfileRequestContext, String> strategy) {
+ checkSetterPreconditions();
+ authenticationAuthorityLookupStrategy = Constraint.isNotNull(strategy,
+ "Authentication Authority Lookup Strategy can not be null");
+ }
+
+ /** {@inheritDoc} */
+ @Nullable public String apply(@Nullable final ProfileRequestContext prc) {
+ checkComponentActive();
+
+
+ if (prc == null) {
+ log.debug("{}: ProfileRequestContext is null, response correlation will fail", getId());
+ return null;
+ }
+
+ // Find the relying party or authenticating authority this request has been sent to.
+ final String authenticationAuthority = authenticationAuthorityLookupStrategy.apply(prc);
+ // Locate the authentication request to pull out information for saving off.
+ final OIDCAuthenticationRequest authnRequest = authenticationRequestLookupStrategy.apply(prc);
+
+ if (authenticationAuthority == null) {
+ log.debug("{}: Authentication authority is null, response correlation will fail", getId());
+ return null;
+ }
+ if (authnRequest == null) {
+ log.debug("{}: Authentication request is null, response correlation will fail", getId());
+ return null;
+ }
+ // Construct a DTO for saving off the authentication request state
+ final AuthenticationRequestStateData state =
+ AuthenticationRequestStateData.from(authnRequest, authenticationAuthority);
+ log.debug("{}: Created authentication request state data: {}", getId(), state);
+
+ try {
+ // Serialize the authentication request state
+ final String serializedState = objectMapper.writeValueAsString(state);
+ if (serializedState == null) {
+ return null;
+ }
+ final DataSealer localDataSealer = dataSealer;
+ if (localDataSealer != null && sealState.test(prc)) {
+ return localDataSealer.wrap(serializedState);
+ } else {
+ return serializedState;
+ }
+ } catch (final JsonProcessingException | DataSealerException e) {
+ log.error("{}: Issue generating authentication request state",getId(),e);
+ }
+ return null;
+ }
+
+}
+
diff --git a/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/AuthenticationRequestTimeLookupFunction.java b/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/AuthenticationRequestTimeLookupFunction.java
new file mode 100644
index 0000000..15c9fe4
--- /dev/null
+++ b/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/AuthenticationRequestTimeLookupFunction.java
@@ -0,0 +1,97 @@
+/*
+ * 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.time.Duration;
+import java.time.Instant;
+import java.util.function.Function;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.NotThreadSafe;
+
+import org.opensaml.profile.context.ProfileRequestContext;
+
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.sp.oidc.context.AuthnRequestStateDataContext;
+import net.shibboleth.sp.oidc.profile.AuthenticationRequestStateData;
+
+/**
+ * Lookup function that returns the time at which the RP sent the authentication request, as taken from the
+ * authentication request state object recovered when the response was obtained from the agent.
+ *
+ * <p>If the time can not be found, <code>null</code> is returned.</p>
+ */
+ at NotThreadSafe
+public class AuthenticationRequestTimeLookupFunction implements Function<ProfileRequestContext, Instant> {
+
+ /**
+ * Strategy used to locate the {@link AuthnRequestStateDataContext}.
+ */
+ @Nonnull private Function<ProfileRequestContext, AuthnRequestStateDataContext> authenticationRequestLookupStrategy;
+
+ /**
+ * A clock skew to apply to the authentication request time. This accounts for differences between system clocks by
+ * treating the authentication request as if it occurred slightly earlier by the specified skew duration. By default
+ * there is no skew.
+ */
+ @Nonnull private Duration clockSkew;
+
+ /** Constructor.*/
+ public AuthenticationRequestTimeLookupFunction() {
+ authenticationRequestLookupStrategy = prc -> {
+ final var inboundMsgCtx = prc.getInboundMessageContext();
+ if (inboundMsgCtx == null) {
+ return null;
+ }
+ return inboundMsgCtx.getSubcontext(AuthnRequestStateDataContext.class);
+ };
+ final Duration ofSeconds = Duration.ofSeconds(0);
+ assert ofSeconds != null;
+ clockSkew = ofSeconds;
+ }
+
+ /**
+ * Set the clock skew.
+ *
+ * @param skew clock skew to set
+ */
+ public void setClockSkew(@Nonnull final Duration skew) {
+ clockSkew = Constraint.isNotNull(skew, "Clock skew cannot be null");
+ }
+
+ /**
+ * Set the strategy used to locate the {@link AuthenticationRequestStateData} to use.
+ *
+ * @param strategy lookup strategy
+ */
+ public void setAuthenticationRequestLookupStrategy(
+ @Nonnull final Function<ProfileRequestContext, AuthnRequestStateDataContext> strategy) {
+ authenticationRequestLookupStrategy =
+ Constraint.isNotNull(strategy, "AuthenticationRequestLookupStrategy lookup strategy cannot be null");
+ }
+
+ @Override
+ @Nullable public Instant apply(@Nullable final ProfileRequestContext input) {
+ if (input == null) {
+ return null;
+ }
+ final AuthnRequestStateDataContext authnRequestCtx = authenticationRequestLookupStrategy.apply(input);
+ final AuthenticationRequestStateData authnRequest = authnRequestCtx != null ? authnRequestCtx.getAuthnState() : null;
+ final Instant authTime = authnRequest != null ? authnRequest.getAuthnRequestTime() : null;
+ return authTime == null ? null : authTime.minus(clockSkew);
+ }
+
+}
diff --git a/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/MaxAgeFromAuthenticationRequestLookupFunction.java b/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/MaxAgeFromAuthenticationRequestLookupFunction.java
new file mode 100644
index 0000000..cf675d1
--- /dev/null
+++ b/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/MaxAgeFromAuthenticationRequestLookupFunction.java
@@ -0,0 +1,117 @@
+/*
+ * 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.time.Duration;
+import java.util.function.Function;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.NotThreadSafe;
+
+import org.opensaml.profile.context.ProfileRequestContext;
+
+import net.shibboleth.oidc.profile.config.OIDCAuthenticationProfileConfiguration;
+import net.shibboleth.profile.config.ProfileConfiguration;
+import net.shibboleth.profile.context.RelyingPartyContext;
+import net.shibboleth.profile.context.navigate.AbstractRelyingPartyLookupFunction;
+import net.shibboleth.shared.annotation.ParameterName;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.sp.oidc.context.AuthnRequestStateDataContext;
+import net.shibboleth.sp.oidc.profile.AuthenticationRequestStateData;
+
+/**
+ * Locate the maximum authentication age from the authentication request. If not present in the request, it is taken
+ * from the profile configuration associated with the relying party context. Returning a default value if neither are
+ * found.
+ */
+ at NotThreadSafe
+public class MaxAgeFromAuthenticationRequestLookupFunction extends AbstractRelyingPartyLookupFunction<Duration> {
+
+ /** Default max authentication age if none can be found on the profile context.*/
+ @Nonnull private final Duration maxAgeDefault;
+
+ /**
+ * Strategy used to locate the {@link AuthnRequestStateDataContext}. Defaults to under the inbound message context.
+ */
+ @Nonnull private Function<ProfileRequestContext, AuthnRequestStateDataContext> authenticationRequestLookupStrategy;
+
+ /**
+ * Constructor.
+ *
+ * @param defaultAge the default value to use for maximum authentication age
+ */
+ public MaxAgeFromAuthenticationRequestLookupFunction(
+ @ParameterName(name = "maxAgeDefault") @Nonnull final Duration defaultAge) {
+ maxAgeDefault = Constraint.isNotNull(defaultAge, "Max Age default can not be null");
+
+ authenticationRequestLookupStrategy = prc -> {
+ final var inboundMsgCtx = prc.getInboundMessageContext();
+ if (inboundMsgCtx == null) {
+ return null;
+ }
+ return inboundMsgCtx.getSubcontext(AuthnRequestStateDataContext.class);
+ };
+ }
+
+ /**
+ * Set the authentication request lookup strategy to use.
+ *
+ * @param strategy the strategy
+ */
+ public void setAuthenticationRequestLookupStrategy(
+ @Nonnull final Function<ProfileRequestContext, AuthnRequestStateDataContext> strategy) {
+ authenticationRequestLookupStrategy = Constraint.isNotNull(strategy,
+ "AuthenticationRequestLookupStrategy can not be null");
+ }
+
+ @Override
+ @Nonnull
+ public Duration apply(@Nullable final ProfileRequestContext input) {
+
+ if (input == null) {
+ return maxAgeDefault;
+ }
+
+ // Max_age from authentication request is authoritative over that from the profile config.
+
+ final AuthnRequestStateDataContext authnRequestCtx = authenticationRequestLookupStrategy.apply(input);
+ final AuthenticationRequestStateData authnRequest =
+ authnRequestCtx != null ? authnRequestCtx.getAuthnState() : null;
+ if (authnRequest != null) {
+ final Duration authnRequestMaxAge = authnRequest.getMaxAge();
+ if (authnRequestMaxAge != null) {
+ return authnRequestMaxAge;
+ }
+ }
+
+ // Check one was not specified in the relying party context
+
+ final RelyingPartyContext rpc = getRelyingPartyContextLookupStrategy().apply(input);
+ if (rpc != null) {
+ final ProfileConfiguration pc = rpc.getProfileConfig();
+ if (pc instanceof final OIDCAuthenticationProfileConfiguration config){
+ final Duration maxAge = config.getMaxAuthenticationAge(input);
+ if (maxAge == null) {
+ return maxAgeDefault;
+ } else {
+ return maxAge;
+ }
+ }
+ }
+ return maxAgeDefault;
+ }
+
+}
diff --git a/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/NonceFromAuthenticationRequestStateLookupStrategy.java b/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/NonceFromAuthenticationRequestStateLookupStrategy.java
new file mode 100644
index 0000000..a55639e
--- /dev/null
+++ b/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/NonceFromAuthenticationRequestStateLookupStrategy.java
@@ -0,0 +1,87 @@
+/*
+ * 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.BiFunction;
+import java.util.function.Function;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.ThreadSafe;
+
+import org.opensaml.messaging.context.MessageContext;
+import org.opensaml.profile.context.ProfileRequestContext;
+
+import com.nimbusds.jwt.JWTClaimsSet;
+
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.primitive.StringSupport;
+import net.shibboleth.sp.oidc.context.AuthnRequestStateDataContext;
+import net.shibboleth.sp.oidc.profile.AuthenticationRequestStateData;
+
+/**
+ * Lookup strategy that finds the id_token nonce from the {@link AuthenticationRequestStateData}.
+ * Returns {@code null} if not found.
+ */
+ at ThreadSafe
+public final class NonceFromAuthenticationRequestStateLookupStrategy
+ implements BiFunction<ProfileRequestContext,JWTClaimsSet, String> {
+
+ /** The strategy used to lookup the {@link AuthenticationRequestStateData}.*/
+ @Nonnull
+ private final Function<ProfileRequestContext, AuthenticationRequestStateData> authenticationStateDataLookupStrategy;
+
+ /**
+ *
+ * Constructor.
+ *
+ * @param strategy the lookup strategy to use
+ */
+ public NonceFromAuthenticationRequestStateLookupStrategy(
+ @Nonnull final Function<ProfileRequestContext, AuthenticationRequestStateData> strategy) {
+ authenticationStateDataLookupStrategy =
+ Constraint.isNotNull(strategy, "Authentication state data lookup strategy can not be null");
+ }
+
+ /** Constructor.*/
+ public NonceFromAuthenticationRequestStateLookupStrategy() {
+ authenticationStateDataLookupStrategy = prc -> {
+ if (prc == null) {
+ return null;
+ }
+ final MessageContext messageContext = prc.getInboundMessageContext();
+ if (messageContext == null) {
+ return null;
+ }
+ final AuthnRequestStateDataContext context = messageContext.getSubcontext(AuthnRequestStateDataContext.class);
+ if (context != null) {
+ return context.getAuthnState();
+ }
+ return null;
+ };
+ }
+
+ @Override @Nullable public String apply(@Nullable final ProfileRequestContext context,
+ @Nullable final JWTClaimsSet claims) {
+
+ final AuthenticationRequestStateData state = authenticationStateDataLookupStrategy.apply(context);
+ final String nonce = state != null ? state.getNonce() : null;
+ if (StringSupport.trimOrNull(nonce) == null) {
+ return null;
+ }
+ return nonce;
+ }
+
+}
diff --git a/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/PrepareOIDCInboundMessageContext.java b/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/PrepareOIDCInboundMessageContext.java
index 3f92aa5..bfcff9c 100644
--- a/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/PrepareOIDCInboundMessageContext.java
+++ b/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/PrepareOIDCInboundMessageContext.java
@@ -45,16 +45,17 @@ import net.shibboleth.shared.primitive.LoggerFactory;
* @event {@link EventIds#INVALID_PROFILE_CTX}
* @post Add an {@link OIDCPeerEntityContext} to the inbound message context.
*/
+// TODO modified fromn the RP version and more appropriate use of relying party id lookup over 'identifier'
public class PrepareOIDCInboundMessageContext extends AbstractProfileAction {
/** Class logger. */
@Nonnull private final Logger log = LoggerFactory.getLogger(PrepareOIDCInboundMessageContext.class);
/** Strategy to lookup the identifier of the OP/RP to base the inbound context on. */
- @NonnullAfterInit private Function<ProfileRequestContext,String> identifierLookupStrategy;
+ @NonnullAfterInit private Function<ProfileRequestContext,String> relyingPartyLookupStrategy;
/** The identifier of the OP/RP to base the inbound context on. */
- @Nullable private String identifier;
+ @Nullable private String relyingPartyId;
/**
* Should the peer entity context be added to an existing inbound message context or not.
@@ -72,8 +73,8 @@ public class PrepareOIDCInboundMessageContext extends AbstractProfileAction {
protected void doInitialize() throws ComponentInitializationException {
super.doInitialize();
- if (identifierLookupStrategy == null) {
- throw new ComponentInitializationException("IdentifierLookupStrategy cannot be null");
+ if (relyingPartyLookupStrategy == null) {
+ throw new ComponentInitializationException("Relying Party Lookup Strategy cannot be null");
}
}
@@ -107,10 +108,10 @@ public class PrepareOIDCInboundMessageContext extends AbstractProfileAction {
*
* @param strategy lookup strategy
*/
- public void setIdentifierLookupStrategy(@Nonnull final Function<ProfileRequestContext,String> strategy) {
+ public void setRelyingPartyLookupStrategy(@Nonnull final Function<ProfileRequestContext,String> strategy) {
checkSetterPreconditions();
- identifierLookupStrategy = Constraint.isNotNull(strategy, "Identifier lookup strategy can not be null");
+ relyingPartyLookupStrategy = Constraint.isNotNull(strategy, "Relying Party Lookup Strategy cannot be null");
}
@Override protected boolean doPreExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
@@ -119,9 +120,9 @@ public class PrepareOIDCInboundMessageContext extends AbstractProfileAction {
return false;
}
- identifier = identifierLookupStrategy.apply(profileRequestContext);
- if (identifier == null) {
- log.warn("{} No identifier returned from lookup function", getLogPrefix());
+ relyingPartyId = relyingPartyLookupStrategy.apply(profileRequestContext);
+ if (relyingPartyId == null) {
+ log.warn("{} No relying party ID returned from lookup function", getLogPrefix());
ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
return false;
}
@@ -148,9 +149,9 @@ public class PrepareOIDCInboundMessageContext extends AbstractProfileAction {
}
final OIDCPeerEntityContext peerContext = msgCtx.ensureSubcontext(OIDCPeerEntityContext.class);
- peerContext.setIdentifier(identifier);
+ peerContext.setIdentifier(relyingPartyId);
- log.debug("{} Initialized inbound context for OIDC authentication message to {}", getLogPrefix(), identifier);
+ log.debug("{} Initialized inbound context for '{}'", getLogPrefix(), relyingPartyId);
}
}
diff --git a/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/ProcessCorrelationCookie.java b/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/ProcessCorrelationCookie.java
index 591e61f..87ec9df 100644
--- a/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/ProcessCorrelationCookie.java
+++ b/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/ProcessCorrelationCookie.java
@@ -14,12 +14,13 @@
package net.shibboleth.sp.oidc.profile.impl;
-import java.util.function.BiConsumer;
+import java.util.function.BiFunction;
import java.util.function.Function;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
+import org.opensaml.profile.action.ActionSupport;
import org.opensaml.profile.action.EventIds;
import org.opensaml.profile.context.ProfileRequestContext;
import org.slf4j.Logger;
@@ -47,6 +48,7 @@ import net.shibboleth.sp.profile.AbstractApplicationAction;
* <p>The value is set onto the context tree using a custom consumer strategy.</p>
*
* TODO this is almost the same as that in sp-server
+ * TODO if used for more than correlation cookies, rename class to just processing cookies?
*
* <p>The information captured is stored in a {@link TokenConsumerContext} child of the
* {@link AgentRequestContext}.</p>
@@ -60,7 +62,7 @@ import net.shibboleth.sp.profile.AbstractApplicationAction;
public class ProcessCorrelationCookie extends AbstractApplicationAction {
/** Default cookie prefix. */
- @Nonnull @NotEmpty static public String DEFAULT_COOKIE_PREFIX = "_shibsp_req_";
+ @Nonnull @NotEmpty static public final String DEFAULT_COOKIE_PREFIX = "_shibsp_req_";
/** Class logger. */
@Nonnull private final Logger log = LoggerFactory.getLogger(ProcessCorrelationCookie.class);
@@ -74,8 +76,15 @@ public class ProcessCorrelationCookie extends AbstractApplicationAction {
/** Lookup strategy for state token value. */
@NonnullAfterInit private Function<ProfileRequestContext,String> stateTokenLookupStrategy;
- /** A consumer to set the cookie value into the appropriate context in the tree.*/
- @Nullable BiConsumer<ProfileRequestContext, String> cookieValueConsumerStrategy;
+ /**
+ * A consumer to set the cookie value into the appropriate context in the tree. The return value indicates
+ * if the consumer function was successful or not. An unsuccessful outcome may trigger an error depending on the
+ * errorFatal flag.
+ */
+ @Nullable BiFunction<ProfileRequestContext, String, Boolean> cookieValueConsumerStrategy;
+
+ /** Whether an error recovering a correlation cookie is fatal. */
+ private boolean errorFatal;
/** State token accompanying request. */
@NonnullBeforeExec private String stateToken;
@@ -97,11 +106,13 @@ public class ProcessCorrelationCookie extends AbstractApplicationAction {
}
/**
- * Set the consumer strategy to use to set the cookie value into the context tree.
+ * Set the consumer function strategy to use to set the cookie value into the context tree. The return value
+ * indicates if the consumer function was successful or not. An unsuccessful outcome may trigger an error
+ * depending on the errorFatal flag. *
*
* @param strategy The strategy to set.
*/
- public void setCookieValueConsumerStrategy(final BiConsumer<ProfileRequestContext, String> strategy) {
+ public void setCookieValueConsumerStrategy(final BiFunction<ProfileRequestContext, String, Boolean> strategy) {
checkSetterPreconditions();
cookieValueConsumerStrategy = Constraint.isNotNull(strategy,
"cookieValueConsumerStrategy can not be null");
@@ -120,6 +131,19 @@ public class ProcessCorrelationCookie extends AbstractApplicationAction {
cookiePrefix = Constraint.isNotNull(StringSupport.trimOrNull(prefix), "Cookie prefix cannot be null or empty");
}
+ /**
+ * Sets whether an error recovering a state token should result in a fatal event.
+ *
+ * <p>Defaults to false.</p>
+ *
+ * @param flag flag to set
+ */
+ public void setErrorFatal(final boolean flag) {
+ checkSetterPreconditions();
+
+ errorFatal = flag;
+ }
+
/**
* Sets the lookup strategy for obtaining the request message's ID.
*
@@ -151,11 +175,11 @@ public class ProcessCorrelationCookie extends AbstractApplicationAction {
}
stateToken = stateTokenLookupStrategy.apply(profileRequestContext);
- if (stateToken == null) {
+ if (stateToken == null && !errorFatal) {
log.debug("{} No state token found in request, skipping correlation cookie processing", getLogPrefix());
return false;
}
-
+ // If error is fatal, we do not skip processing here.
return true;
}
@@ -175,15 +199,26 @@ public class ProcessCorrelationCookie extends AbstractApplicationAction {
final String cookieName = cookiePrefix + escaper.escape(stateToken);
final String value = cookieManager.getCookieValue(cookieName, null);
- if (value == null) {
+ if (value == null && !errorFatal) {
log.debug("{} No correlation cookie found for state token '{}'", getLogPrefix(), stateToken);
return;
+ } else if (value == null){
+ log.warn("{} No correlation cookie found for state token '{}'", getLogPrefix(), stateToken);
+ ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_MESSAGE);
+ return;
}
cookieManager.unsetCookie(cookieName);
if (cookieValueConsumerStrategy != null) {
- cookieValueConsumerStrategy.accept(profileRequestContext, value);
+ final Boolean success = cookieValueConsumerStrategy.apply(profileRequestContext, value);
+ // Treat null as failure
+ if ((success == null || Boolean.FALSE.equals(success)) && errorFatal) {
+ log.warn("{} Could not set correlation cookie", getLogPrefix());
+ ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_MESSAGE);
+ return;
+ }
+ log.debug("{} Processed correlation cookie for state token '{}'", getLogPrefix(), stateToken);
} else {
log.warn("{} No cookie value consumer strategy defined, cookie value not processed", getLogPrefix());
}
diff --git a/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/SetAuthenticationRequestToPeerContextConsumer.java b/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/SetAuthenticationRequestToPeerContextConsumer.java
new file mode 100644
index 0000000..f0c609e
--- /dev/null
+++ b/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/SetAuthenticationRequestToPeerContextConsumer.java
@@ -0,0 +1,170 @@
+/*
+ * 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.BiFunction;
+import java.util.function.Predicate;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.messaging.context.MessageContext;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.slf4j.Logger;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+import net.shibboleth.oidc.profile.messaging.context.OIDCPeerEntityContext;
+import net.shibboleth.shared.annotation.constraint.NonnullAfterInit;
+import net.shibboleth.shared.component.AbstractIdentifiableInitializableComponent;
+import net.shibboleth.shared.component.ComponentInitializationException;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.logic.PredicateSupport;
+import net.shibboleth.shared.net.URISupport;
+import net.shibboleth.shared.primitive.LoggerFactory;
+import net.shibboleth.shared.security.DataSealer;
+import net.shibboleth.shared.security.DataSealerException;
+import net.shibboleth.sp.oidc.context.AuthnRequestStateDataContext;
+import net.shibboleth.sp.oidc.profile.AuthenticationRequestStateData;
+
+/**
+ * A consumer function that converts the decoded authentication request state data JSON string into an object and sets it
+ * onto an {@link AuthnRequestStateDataContext} within the inbound message context, along with setting the issuer onto
+ * the {@link OIDCPeerEntityContext}.
+ */
+//TODO this class is complex and more fitting a decoder type mechanism
+public class SetAuthenticationRequestToPeerContextConsumer extends AbstractIdentifiableInitializableComponent
+ implements BiFunction<ProfileRequestContext, String, Boolean> {
+
+ /** Class logger. */
+ @Nonnull private final Logger log = LoggerFactory.getLogger(SetAuthenticationRequestToPeerContextConsumer.class);
+
+ /** JSON object mapper. */
+ @NonnullAfterInit private ObjectMapper objectMapper;
+
+ /** Optional data sealer to use. */
+ @Nullable private DataSealer dataSealer;
+
+ /**
+ * If the dataSealer is provided should it be used to unwrap the authentication request state? Defaults to true,
+ * that is, if the dataSealer is provided, always attempt to unwrap the state.
+ */
+ @Nonnull private Predicate<ProfileRequestContext> unsealState;
+
+ /** Constructor.*/
+ public SetAuthenticationRequestToPeerContextConsumer() {
+ unsealState = PredicateSupport.alwaysTrue();
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected void doInitialize() throws ComponentInitializationException {
+ super.doInitialize();
+
+ if (objectMapper == null) {
+ throw new ComponentInitializationException("ObjectMapper cannot be null");
+ }
+ }
+
+ /**
+ * Set the JSON {@link ObjectMapper} to use for serialization.
+ *
+ * @param mapper object mapper
+ */
+ public void setObjectMapper(@Nonnull final ObjectMapper mapper) {
+ checkSetterPreconditions();
+
+ objectMapper = Constraint.isNotNull(mapper, "ObjectMapper cannot be null");
+ }
+
+ /**
+ * Sets {@link DataSealer} to use.
+ *
+ * @param sealer data sealer
+ */
+ public void setDataSealer(@Nullable final DataSealer sealer) {
+ checkSetterPreconditions();
+
+ dataSealer = sealer;
+ }
+
+ /**
+ * Set the predicate to determine whether to unseal the state.
+ *
+ * @param predicate the seal state predicate to set.
+ */
+ public void setUnsealStatePredicate(@Nonnull final Predicate<ProfileRequestContext> predicate) {
+ checkSetterPreconditions();
+
+ unsealState = Constraint.isNotNull(predicate, "Seal state predicate can not be null");
+ }
+
+ /**
+ * Set the flag to determine whether to unseal the state.
+ *
+ * @param flag the flag to set.
+ */
+ public void setUnsealState(final boolean flag) {
+ checkSetterPreconditions();
+
+ unsealState = flag ? PredicateSupport.alwaysTrue() : PredicateSupport.alwaysFalse();
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public Boolean apply(@Nullable final ProfileRequestContext profileRequestContext, @Nullable final String value) {
+ checkComponentActive();
+ if (profileRequestContext == null || value == null) {
+ log.trace("Authentication request value could not be decoded, the input value was null");
+ return false;
+ }
+ String decoded = null;
+ try {
+ decoded = URISupport.doURLDecode(value);
+ } catch (final Exception e) {
+ log.trace("Authentication request value could not be decoded", e);
+ }
+ if (decoded == null) {
+ log.trace("Authentication request value was not decoded");
+ return false;
+ }
+ // Possibly unseal and convert to class
+ try {
+ if (unsealState.test(profileRequestContext) && dataSealer != null) {
+ decoded = dataSealer.unwrap(decoded);
+ }
+ final AuthenticationRequestStateData authnState = objectMapper.readValue(decoded, AuthenticationRequestStateData.class);
+ log.debug("Recovered authentication request state '{}'", authnState);
+ final MessageContext inboundCtx = profileRequestContext.getInboundMessageContext();
+ if (inboundCtx == null) {
+ log.trace("There is no Inbound Context, cannot set authentication request data");
+ return false;
+ }
+ // Add identifier to the peer context now we know it
+ inboundCtx.ensureSubcontext(OIDCPeerEntityContext.class)
+ .setIdentifier(authnState.getAuthenticationAuthority());
+ // Attach the recovered authentication request state information to the inbound context for later use
+ inboundCtx.ensureSubcontext(AuthnRequestStateDataContext.class).setAuthnState(authnState);
+ return true;
+
+ } catch (final JsonProcessingException | DataSealerException e) {
+ log.error("Unable to process authentication request state data, was it sealed and you are not unsealing it?"
+ , e);
+ return false;
+ }
+ }
+
+}
diff --git a/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/SetNonceValueToTokenContextConsumer.java b/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/SetNonceValueToTokenContextConsumer.java
index b20016d..2d7b7f1 100644
--- a/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/SetNonceValueToTokenContextConsumer.java
+++ b/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/SetNonceValueToTokenContextConsumer.java
@@ -14,7 +14,7 @@
package net.shibboleth.sp.oidc.profile.impl;
-import java.util.function.BiConsumer;
+import java.util.function.BiFunction;
import javax.annotation.Nonnull;
@@ -28,15 +28,17 @@ import net.shibboleth.sp.context.TokenConsumerContext;
/**
* A consumer that sets the decoded nonce value onto the token context's message correlation ID.
+ *
+ * TODO, check this is a sensible place to put the nonce, if so, document why a nonce here not a correlation ID.
*/
-public class SetNonceValueToTokenContextConsumer implements BiConsumer<ProfileRequestContext, String> {
+public class SetNonceValueToTokenContextConsumer implements BiFunction<ProfileRequestContext, String, Boolean> {
/** Class logger. */
@Nonnull private final Logger log = LoggerFactory.getLogger(SetNonceValueToTokenContextConsumer.class);
/** {@inheritDoc} */
@Override
- public void accept(final ProfileRequestContext profileRequestContext, final String value) {
+ public Boolean apply(final ProfileRequestContext profileRequestContext, final String value) {
final String decoded = URISupport.doURLDecode(value);
@@ -44,6 +46,7 @@ public class SetNonceValueToTokenContextConsumer implements BiConsumer<ProfileRe
profileRequestContext.ensureSubcontext(AgentRequestContext.class)
.ensureSubcontext(TokenConsumerContext.class).setMessageCorrelationID(decoded);
+ return true;
}
diff --git a/sp-oidc-impl/src/test/java/net/shibboleth/sp/oidc/profile/impl/AuthTimeRequestedActivationConditionTest.java b/sp-oidc-impl/src/test/java/net/shibboleth/sp/oidc/profile/impl/AuthTimeRequestedActivationConditionTest.java
new file mode 100644
index 0000000..6d669e0
--- /dev/null
+++ b/sp-oidc-impl/src/test/java/net/shibboleth/sp/oidc/profile/impl/AuthTimeRequestedActivationConditionTest.java
@@ -0,0 +1,102 @@
+/*
+ * 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 static org.testng.Assert.assertFalse;
+import static org.testng.Assert.assertTrue;
+
+import org.testng.annotations.Test;
+
+import com.nimbusds.jwt.JWTClaimsSet;
+
+import net.shibboleth.sp.oidc.context.AuthnRequestStateDataContext;
+import net.shibboleth.sp.oidc.profile.AuthenticationRequestStateData;
+import net.shibboleth.sp.profile.impl.BaseAgplicationActionTest;
+
+/**
+ * Tests for {@link AuthTimeRequestedActivationCondition}
+ */
+public class AuthTimeRequestedActivationConditionTest extends BaseAgplicationActionTest {
+
+ /** The condition to test.*/
+ private AuthTimeRequestedActivationCondition condition;
+
+
+ @Test
+ public void testAuthTimeRequested() {
+ final AuthenticationRequestStateData state = new AuthenticationRequestStateData()
+ .setAuthTimeRequired(true);
+ condition = new AuthTimeRequestedActivationCondition(prc ->
+ new AuthnRequestStateDataContext().setAuthnState(state));
+
+ final boolean requested = condition.test(prc, null);
+ assertTrue(requested);
+
+ }
+
+
+ @Test
+ public void testAuthTimeNotRequested() {
+ final AuthenticationRequestStateData state = new AuthenticationRequestStateData()
+ .setAuthTimeRequired(false);
+
+ condition = new AuthTimeRequestedActivationCondition(
+ prcArg -> new AuthnRequestStateDataContext().setAuthnState(state)
+ );
+
+ final boolean requested = condition.test(prc, null);
+ assertFalse(requested);
+ }
+
+ @Test
+ public void testFailSecureWhenContextLookupReturnsNull() {
+ // Lookup strategy returns null -> should default to true (fail-secure)
+ condition = new AuthTimeRequestedActivationCondition(
+ prcArg -> null
+ );
+
+ final boolean requested = condition.test(prc, null);
+ assertTrue(requested, "Should default to true when AuthnRequestStateDataContext is missing");
+ }
+
+ @Test
+ public void testFailSecureWhenAuthnStateIsNull() {
+ // Context present but authnState is null -> should default to true (fail-secure)
+ condition = new AuthTimeRequestedActivationCondition(
+ prcArg -> new AuthnRequestStateDataContext().setAuthnState(null)
+ );
+
+ final boolean requested = condition.test(prc, null);
+ assertTrue(requested, "Should default to true when authnState is null");
+ }
+
+ @Test
+ public void testClaimsAreIgnored() {
+ final AuthenticationRequestStateData state = new AuthenticationRequestStateData()
+ .setAuthTimeRequired(true);
+
+ condition = new AuthTimeRequestedActivationCondition(
+ prcArg -> new AuthnRequestStateDataContext().setAuthnState(state)
+ );
+
+ final JWTClaimsSet claims1 = new JWTClaimsSet.Builder().claim("foo", "bar").build();
+ final JWTClaimsSet claims2 = new JWTClaimsSet.Builder().claim("baz", 123).build();
+
+ assertTrue(condition.test(prc, claims1));
+ assertTrue(condition.test(prc, claims2));
+ }
+
+
+}
diff --git a/sp-oidc-impl/src/test/java/net/shibboleth/sp/oidc/profile/impl/AuthenticationRequestStateForStorageStrategyTest.java b/sp-oidc-impl/src/test/java/net/shibboleth/sp/oidc/profile/impl/AuthenticationRequestStateForStorageStrategyTest.java
new file mode 100644
index 0000000..a9f1a38
--- /dev/null
+++ b/sp-oidc-impl/src/test/java/net/shibboleth/sp/oidc/profile/impl/AuthenticationRequestStateForStorageStrategyTest.java
@@ -0,0 +1,180 @@
+/*
+ * 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 static org.testng.Assert.assertFalse;
+import static org.testng.Assert.assertNotNull;
+import static org.testng.Assert.assertNull;
+import static org.testng.Assert.assertTrue;
+
+import java.time.Duration;
+import java.time.Instant;
+
+import org.mockito.Mockito;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import com.fasterxml.jackson.annotation.JsonInclude.Include;
+import com.fasterxml.jackson.core.Base64Variants;
+import com.fasterxml.jackson.databind.DeserializationFeature;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.json.JsonMapper;
+import com.fasterxml.jackson.datatype.jdk8.Jdk8Module;
+import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
+import com.nimbusds.oauth2.sdk.id.ClientID;
+import com.nimbusds.openid.connect.sdk.Nonce;
+
+import net.shibboleth.oidc.profile.core.OIDCAuthenticationRequest;
+import net.shibboleth.shared.component.ComponentInitializationException;
+import net.shibboleth.shared.security.DataSealer;
+import net.shibboleth.sp.profile.impl.BaseAgplicationActionTest;
+
+/**
+ * Tests for {@link AuthenticationRequestStateForStorageStrategy}.
+ */
+public class AuthenticationRequestStateForStorageStrategyTest extends BaseAgplicationActionTest{
+
+ /** The strategy to test.*/
+ private AuthenticationRequestStateForStorageStrategy strategy;
+
+ /** The authentication request.*/
+ private OIDCAuthenticationRequest authnRequest;
+
+ /** The ObjectMapper to use.*/
+ private ObjectMapper mapper;
+
+ /** The time when the request was constructed in the before method.*/
+ private Instant now;
+
+ private Duration maxAge;
+
+ @Override
+ @BeforeMethod
+ public void beforeMethod() throws ComponentInitializationException {
+ super.beforeMethod();
+
+ mapper = JsonMapper.builder()
+ .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
+ .serializationInclusion(Include.NON_ABSENT)
+ .defaultBase64Variant(Base64Variants.MODIFIED_FOR_URL)
+ .addModule(new Jdk8Module())
+ .addModule(new JavaTimeModule())
+ .build();
+
+ strategy = new AuthenticationRequestStateForStorageStrategy();
+ strategy.setId("TestComponent");
+ strategy.setAuthenticationAuthorityLookupStrategy(prc -> "https://op.example.com");
+ authnRequest = new OIDCAuthenticationRequest(new ClientID("client-id"));
+ authnRequest.setNonce(new Nonce("nonce"));
+ authnRequest.setCodeVerifier("PKCE");
+ now = Instant.now();
+ authnRequest.setAuthnRequestTime(now);
+ maxAge = Duration.ofMinutes(1);
+ authnRequest.setMaxAge(maxAge);
+
+ strategy.setAuthenticationRequestLookupStrategy(prc -> authnRequest);
+ strategy.setObjectMapper(mapper);
+ }
+
+ @Test
+ public void testSuccessfulConstruction() throws Exception {
+ strategy.initialize();
+ final String requestState = strategy.apply(prc);
+
+ final JsonNode root = mapper.readTree(requestState);
+ assertTrue(root.hasNonNull("nonce"), "Nonce should be present and non-null");
+ assertTrue(root.hasNonNull("authority"), "Issuer should be present and non-null");
+ assertTrue(root.hasNonNull("pkce_code_verifier"), "Pkce_code_verifier should be present and non-null");
+ assertTrue(root.hasNonNull("authn_request_time"), "authn_request_time should be present and non-null");
+ assertTrue(root.hasNonNull("max_age"), "max_age should be present and non-null");
+ assertTrue(root.hasNonNull("is_auth_time_required"), "is_auth_time_required should be present and non-null");
+ assertTrue(root.get("is_auth_time_required").asBoolean(),"is_auth_time_required should be required");
+ }
+
+ @Test
+ public void testFailedConstruction_NoAuthenticatingAuthority() throws Exception {
+ strategy.setAuthenticationAuthorityLookupStrategy(prc -> null);
+ strategy.initialize();
+ final String requestState = strategy.apply(prc);
+
+ assertNull(requestState, "no authenticating authority should result in a null construction");
+ }
+
+ @Test
+ public void testFailedConstruction_NoAuthenticationRequest() throws Exception {
+ strategy.setAuthenticationRequestLookupStrategy(prc -> null);
+ strategy.initialize();
+ final String requestState = strategy.apply(prc);
+
+ assertNull(requestState, "no authentication request should result in a null construction");
+ }
+
+ @Test
+ public void testSuccessfulConstruction_NoPKCE() throws Exception {
+ authnRequest.setCodeVerifier(null);
+ strategy.initialize();
+ final String requestState = strategy.apply(prc);
+
+ final JsonNode root = mapper.readTree(requestState);
+ assertTrue(root.hasNonNull("nonce"), "Nonce should be present and non-null");
+ assertTrue(root.hasNonNull("authority"), "Issuer should be present and non-null");
+ assertNull(root.get("pkce_code_verifier"), "Pkce_code_verifier should be null");
+ assertTrue(root.hasNonNull("authn_request_time"), "authn_request_time should be present and non-null");
+ assertTrue(root.hasNonNull("max_age"), "max_age should be present and non-null");
+ assertTrue(root.hasNonNull("is_auth_time_required"), "is_auth_time_required should be present and non-null");
+ assertTrue(root.get("is_auth_time_required").asBoolean(),"is_auth_time_required should be required");
+
+ }
+
+ @Test
+ public void testSuccessfulConstruction_WithDataSealer() throws Exception {
+ final DataSealer sealer = Mockito.mock(DataSealer.class);
+ Mockito.when(sealer.wrap(Mockito.anyString())).thenReturn("sealed");
+ strategy.setDataSealer(sealer);
+ strategy.initialize();
+ final String requestState = strategy.apply(prc);
+ assertNotNull(requestState, "request state should not be null");
+ assert requestState != null;
+ assertTrue(requestState.contains("sealed"), "request state should be sealed");
+
+ }
+
+ @Test
+ public void testSuccessfulConstruction_WithDataSealerButSealedPredicateIsFalse() throws Exception {
+ final DataSealer sealer = Mockito.mock(DataSealer.class);
+ Mockito.when(sealer.wrap(Mockito.anyString())).thenReturn("sealed");
+ strategy.setDataSealer(sealer);
+ strategy.setSealStatePredicate(prc -> false);
+ strategy.initialize();
+
+ final String requestState = strategy.apply(prc);
+
+ assertNotNull(requestState, "request state should not be null");
+ assert requestState != null;
+ assertFalse(requestState.contains("sealed"), "request state should be sealed");
+
+ final JsonNode root = mapper.readTree(requestState);
+ assertTrue(root.hasNonNull("nonce"), "Nonce should be present and non-null");
+ assertTrue(root.hasNonNull("authority"), "Issuer should be present and non-null");
+ assertTrue(root.hasNonNull("pkce_code_verifier"), "Pkce_code_verifier should be present and non-null");
+ assertTrue(root.hasNonNull("authn_request_time"), "authn_request_time should be present and non-null");
+ assertTrue(root.hasNonNull("max_age"), "max_age should be present and non-null");
+ assertTrue(root.hasNonNull("is_auth_time_required"), "is_auth_time_required should be present and non-null");
+ assertTrue(root.get("is_auth_time_required").asBoolean(),"is_auth_time_required should be required");
+
+ }
+
+}
diff --git a/sp-oidc-impl/src/test/java/net/shibboleth/sp/oidc/profile/impl/AuthenticationRequestTimeLookupFunctionTest.java b/sp-oidc-impl/src/test/java/net/shibboleth/sp/oidc/profile/impl/AuthenticationRequestTimeLookupFunctionTest.java
new file mode 100644
index 0000000..d697468
--- /dev/null
+++ b/sp-oidc-impl/src/test/java/net/shibboleth/sp/oidc/profile/impl/AuthenticationRequestTimeLookupFunctionTest.java
@@ -0,0 +1,111 @@
+/*
+ * 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 static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertNull;
+
+import java.time.Duration;
+import java.time.Instant;
+
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.testng.annotations.Test;
+
+import net.shibboleth.sp.oidc.context.AuthnRequestStateDataContext;
+import net.shibboleth.sp.oidc.profile.AuthenticationRequestStateData;
+
+/**
+ * Tests for {@link AuthenticationRequestTimeLookupFunction}.
+ */
+public class AuthenticationRequestTimeLookupFunctionTest {
+
+ /**
+ * Tests the authentication request time comes from the state.
+ */
+ @Test
+ public void testAuthTimeReturned() {
+ final Instant now = Instant.now();
+
+ final AuthenticationRequestStateData state = new AuthenticationRequestStateData().setAuthnRequestTime(now);
+
+ final AuthenticationRequestTimeLookupFunction function = new AuthenticationRequestTimeLookupFunction();
+
+ function.setAuthenticationRequestLookupStrategy(prc -> new AuthnRequestStateDataContext().setAuthnState(state));
+
+ final Instant result = function.apply(new ProfileRequestContext());
+ assertEquals(result, now);
+ }
+
+ /**
+ * Tests the authentication request time comes from the state minus the clock skew
+ */
+ @Test
+ public void testAuthTimeMinusClockSkew() {
+ final Instant now = Instant.now();
+ final Duration skew = Duration.ofSeconds(10);
+
+ final AuthenticationRequestStateData state = new AuthenticationRequestStateData().setAuthnRequestTime(now);
+
+ final AuthenticationRequestTimeLookupFunction function = new AuthenticationRequestTimeLookupFunction();
+ assert skew != null;
+ function.setClockSkew(skew);
+
+ function.setAuthenticationRequestLookupStrategy(prc -> new AuthnRequestStateDataContext().setAuthnState(state));
+
+ final Instant result = function.apply(new ProfileRequestContext());
+ assertEquals(result, now.minus(skew));
+ }
+
+ /**
+ * Tests no authentication request time in the state.
+ */
+ @Test
+ public void testNoAuthTimeReturnsNull() {
+ final AuthenticationRequestStateData state = new AuthenticationRequestStateData(); // no authn time set
+
+ final AuthenticationRequestTimeLookupFunction function = new AuthenticationRequestTimeLookupFunction();
+
+ function.setAuthenticationRequestLookupStrategy(prc -> new AuthnRequestStateDataContext().setAuthnState(state));
+
+ final Instant result = function.apply(new ProfileRequestContext());
+ assertNull(result);
+ }
+
+ /**
+ * Tests the the lookup strategy returns null.
+ */
+ @Test
+ public void testNoAuthnContextReturnsNull() {
+ final AuthenticationRequestTimeLookupFunction function = new AuthenticationRequestTimeLookupFunction();
+
+ function.setAuthenticationRequestLookupStrategy(prc -> null);
+
+ final Instant result = function.apply(new ProfileRequestContext());
+ assertNull(result);
+ }
+
+ /**
+ * Tests a null profile request context input produces a null result.
+ */
+ @Test
+ public void testNullProfileRequestContextReturnsNull() {
+ final AuthenticationRequestTimeLookupFunction function = new AuthenticationRequestTimeLookupFunction();
+
+ final Instant result = function.apply(null);
+ assertNull(result);
+ }
+
+
+}
diff --git a/sp-oidc-impl/src/test/java/net/shibboleth/sp/oidc/profile/impl/MaxAgeFromAuthenticationRequestLookupFunctionTest.java b/sp-oidc-impl/src/test/java/net/shibboleth/sp/oidc/profile/impl/MaxAgeFromAuthenticationRequestLookupFunctionTest.java
new file mode 100644
index 0000000..8dbb0a2
--- /dev/null
+++ b/sp-oidc-impl/src/test/java/net/shibboleth/sp/oidc/profile/impl/MaxAgeFromAuthenticationRequestLookupFunctionTest.java
@@ -0,0 +1,121 @@
+/*
+ * 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 static org.testng.Assert.assertEquals;
+
+import java.time.Duration;
+
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.testng.annotations.Test;
+
+import net.shibboleth.oidc.profile.config.impl.DefaultOIDCAuthorizationConfiguration;
+import net.shibboleth.profile.context.RelyingPartyContext;
+import net.shibboleth.sp.oidc.context.AuthnRequestStateDataContext;
+import net.shibboleth.sp.oidc.profile.AuthenticationRequestStateData;
+
+/**
+ * Tests for {@link MaxAgeFromAuthenticationRequestLookupFunction}.
+ */
+public class MaxAgeFromAuthenticationRequestLookupFunctionTest {
+
+ /**
+ * Tests max_age present in authentication request.
+ */
+ @Test
+ public void testMaxAgeFromAuthenticationRequest() {
+ final Duration expected = Duration.ofSeconds(600);
+
+ final AuthenticationRequestStateData state = new AuthenticationRequestStateData().setMaxAge(expected);
+
+ final MaxAgeFromAuthenticationRequestLookupFunction function =
+ new MaxAgeFromAuthenticationRequestLookupFunction(Duration.ofSeconds(120));
+
+ function.setAuthenticationRequestLookupStrategy(prc -> new AuthnRequestStateDataContext().setAuthnState(state));
+
+ final Duration result = function.apply(new ProfileRequestContext());
+ assertEquals(result, expected);
+ }
+
+ /**
+ * Test no max_age present in authentication request, so fallback to profile configuration (not the default value).
+ */
+ @Test
+ public void testFallbackToProfileConfiguration() {
+ final Duration expected = Duration.ofSeconds(300);
+
+ final MaxAgeFromAuthenticationRequestLookupFunction func = new MaxAgeFromAuthenticationRequestLookupFunction(
+ Duration.ofSeconds(999));
+
+ func.setAuthenticationRequestLookupStrategy(prc -> null); // no authn request → fallback path
+
+ final ProfileRequestContext prc = new ProfileRequestContext();
+
+ final RelyingPartyContext rpc = prc.ensureSubcontext(RelyingPartyContext.class);
+ final DefaultOIDCAuthorizationConfiguration config = new DefaultOIDCAuthorizationConfiguration();
+ config.setMaxAuthenticationAge(expected);
+ rpc.setProfileConfig(config);
+
+ final Duration result = func.apply(prc);
+ assertEquals(result, expected);
+ }
+
+ @Test
+ public void testFallbackToDefault_WhenNoAuthnRequestOrRPC() {
+ final Duration defaultValue = Duration.ofSeconds(180);
+
+ final MaxAgeFromAuthenticationRequestLookupFunction function =
+ new MaxAgeFromAuthenticationRequestLookupFunction(
+ defaultValue);
+
+ function.setAuthenticationRequestLookupStrategy(prc -> null);
+
+ final Duration result = function.apply(new ProfileRequestContext());
+ assertEquals(result, defaultValue);
+ }
+
+ /**
+ * Tests no max_age in authn request, no max_age in RPC, so default used.
+ */
+ @Test
+ public void testAuthnRequestPresent_ButNoMaxAgeThenRPCDefinesNoneThenDefault() {
+ final Duration defaultValue = Duration.ofSeconds(75);
+
+ final AuthenticationRequestStateData state = new AuthenticationRequestStateData();
+
+ final MaxAgeFromAuthenticationRequestLookupFunction function =
+ new MaxAgeFromAuthenticationRequestLookupFunction(
+ defaultValue);
+
+ function.setAuthenticationRequestLookupStrategy(prc -> new AuthnRequestStateDataContext().setAuthnState(state));
+
+ final ProfileRequestContext prc = new ProfileRequestContext();
+
+ final RelyingPartyContext rpc = prc.ensureSubcontext(RelyingPartyContext.class);
+
+ final DefaultOIDCAuthorizationConfiguration config = new DefaultOIDCAuthorizationConfiguration() {
+ @Override
+ public Duration getMaxAuthenticationAge(final ProfileRequestContext ctx) {
+ return null;
+ }
+ };
+
+ rpc.setProfileConfig(config);
+
+ final Duration result = function.apply(prc);
+ assertEquals(result, defaultValue);
+ }
+
+}
diff --git a/sp-oidc-impl/src/test/java/net/shibboleth/sp/oidc/profile/impl/SetAuthenticationRequestToPeerContextConsumerTest.java b/sp-oidc-impl/src/test/java/net/shibboleth/sp/oidc/profile/impl/SetAuthenticationRequestToPeerContextConsumerTest.java
new file mode 100644
index 0000000..1ee91f1
--- /dev/null
+++ b/sp-oidc-impl/src/test/java/net/shibboleth/sp/oidc/profile/impl/SetAuthenticationRequestToPeerContextConsumerTest.java
@@ -0,0 +1,139 @@
+/*
+ * 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 static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertNotNull;
+import static org.testng.Assert.assertNull;
+
+import org.mockito.Mockito;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+import net.shibboleth.oidc.profile.messaging.context.OIDCPeerEntityContext;
+import net.shibboleth.shared.component.ComponentInitializationException;
+import net.shibboleth.shared.security.DataSealer;
+import net.shibboleth.sp.oidc.context.AuthnRequestStateDataContext;
+import net.shibboleth.sp.oidc.profile.AuthenticationRequestStateData;
+import net.shibboleth.sp.profile.impl.BaseAgplicationActionTest;
+
+/**
+ * Tests for {@link SetAuthenticationRequestToPeerContextConsumer}.
+ */
+public class SetAuthenticationRequestToPeerContextConsumerTest extends BaseAgplicationActionTest{
+
+ /** The Consumer, which is a function, to test.*/
+ private SetAuthenticationRequestToPeerContextConsumer function;
+
+ /** The authentication state data used to construct and suitable JSON input string for deserialising.*/
+ private AuthenticationRequestStateData authnState;
+
+ /** The serialized form of the authnState.*/
+ private String authnStateSerialized;
+
+ @Override
+ @BeforeMethod
+ public void beforeMethod() throws ComponentInitializationException {
+ super.beforeMethod();
+
+ function = new SetAuthenticationRequestToPeerContextConsumer();
+ function.setId("TestComponent");
+
+ authnState = new AuthenticationRequestStateData();
+ authnState.setAuthenticationAuthority("https://op.example.org");
+ authnState.setPkceCodeVerifier("PKCECodeVerifier");
+ authnState.setNonce("nonce");
+
+ final ObjectMapper mapper = new ObjectMapper();
+ function.setObjectMapper(mapper);
+ try {
+ authnStateSerialized = mapper.writeValueAsString(authnState);
+ } catch (final JsonProcessingException e) {
+ throw new ComponentInitializationException(e);
+ }
+
+ }
+
+ @Test
+ public void testSuccess() throws Exception {
+ function.initialize();
+
+ function.apply(prc, authnStateSerialized);
+
+ final var inboundMsgCtx = prc.getInboundMessageContext();
+ assertNotNull(inboundMsgCtx);
+ assert inboundMsgCtx != null;
+ final var peerCtx = inboundMsgCtx.getSubcontext(OIDCPeerEntityContext.class);
+ assertNotNull(peerCtx);
+ assert peerCtx != null;
+ assertEquals(peerCtx.getIdentifier(), "https://op.example.org");
+ final var authnStateCtx = inboundMsgCtx.getSubcontext(AuthnRequestStateDataContext.class);
+ assertNotNull(authnStateCtx);
+ assert authnStateCtx != null;
+ assertNotNull(authnStateCtx.getAuthnState());
+ final var authnStateFromCtx = authnStateCtx.getAuthnState();
+ assertNotNull(authnStateFromCtx);
+ assert authnStateFromCtx != null;
+ assertEquals(authnStateFromCtx.getNonce(), "nonce");
+ assertEquals(authnStateFromCtx.getPkceCodeVerifier(), "PKCECodeVerifier");
+ }
+
+ @Test
+ public void testSuccess_SealedWithDataSealer() throws Exception {
+ final DataSealer sealer = Mockito.mock(DataSealer.class);
+ Mockito.when(sealer.unwrap(Mockito.anyString())).thenReturn(authnStateSerialized);
+ function.setDataSealer(sealer);
+ function.initialize();
+
+ function.apply(prc, authnStateSerialized);
+
+ final var inboundMsgCtx = prc.getInboundMessageContext();
+ assertNotNull(inboundMsgCtx);
+ assert inboundMsgCtx != null;
+ final var peerCtx = inboundMsgCtx.getSubcontext(OIDCPeerEntityContext.class);
+ assertNotNull(peerCtx);
+ assert peerCtx != null;
+ assertEquals(peerCtx.getIdentifier(), "https://op.example.org");
+ final var authnStateCtx = inboundMsgCtx.getSubcontext(AuthnRequestStateDataContext.class);
+ assertNotNull(authnStateCtx);
+ assert authnStateCtx != null;
+ assertNotNull(authnStateCtx.getAuthnState());
+ final var authnStateFromCtx = authnStateCtx.getAuthnState();
+ assertNotNull(authnStateFromCtx);
+ assert authnStateFromCtx != null;
+ assertEquals(authnStateFromCtx.getNonce(), "nonce");
+ assertEquals(authnStateFromCtx.getPkceCodeVerifier(), "PKCECodeVerifier");
+ }
+
+ @Test
+ public void testFail_BadJSONAuthnState() throws Exception {
+ function.initialize();
+
+ function.apply(prc, "bad");
+
+ final var inboundMsgCtx = prc.getInboundMessageContext();
+ assertNotNull(inboundMsgCtx);
+ assert inboundMsgCtx != null;
+ final var peerCtx = inboundMsgCtx.getSubcontext(OIDCPeerEntityContext.class);
+ assertNull(peerCtx);
+ final var authnStateCtx = inboundMsgCtx.getSubcontext(AuthnRequestStateDataContext.class);
+ assertNull(authnStateCtx);
+
+ }
+
+}
--
To stop receiving notification emails like this one, please contact
the administrator of this repository.
More information about the commits
mailing list