[java-plugin-shibd-oidc] branch main updated: JSHIBDOIDC-5 - Add correlation cookie issuance
Codeberg
noreply at shibboleth.net
Fri Apr 10 16:37:30 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/0b8003ee0ae0cfe5608644718574c3ff26296bfd
The following commit(s) were added to refs/heads/main by this push:
new 0b8003e JSHIBDOIDC-5 - Add correlation cookie issuance
0b8003e is described below
commit 0b8003ee0ae0cfe5608644718574c3ff26296bfd
Author: Phil Smart <philip.smart at jisc.ac.uk>
AuthorDate: Fri Apr 10 17:13:37 2026 +0100
JSHIBDOIDC-5 - Add correlation cookie issuance
- Seal and base64URL-encode OAuth state using DataSealer to prevent
tampering
- Preserve authentication and authorization context via
StateTokenManager as sealed JSON. Previously this was done via the
cookie manager
- Change name of CSRF token 'nonce' to request forgery protection rfp.
- Add preserved authnetication and authorization context state token to
correleation cookie alongside rfp value, for later recovery of state value
- Decode and unseal OAuth state on response (via a decoder strategy);
recover resource URL, correlation cookie, and authn context
- Improve recovery of cookies and values, set into appropriate contexts
- Needs more unit tests
https://shibboleth.atlassian.net/browse/JSHIBDOIDC-5
---
.../oidc/context/AuthnRequestStateDataContext.java | 33 ++-
.../context/CorrelationCookieStateContext.java | 54 ++++
...nStateFromCorrelationCookieLookupFunction.java} | 23 +-
...omAuthenticationRequestStateLookupFunction.java | 53 ----
...=> RFPFromCorrelationCookieLookupFunction.java} | 23 +-
...n.java => RFPFromOAuthStateLookupFunction.java} | 6 +-
.../RequestCorrelationFromStateLookupFunction.java | 184 ++++++++++++
....java => StateFromJSONStateLookupFunction.java} | 2 +-
.../shibboleth/sp/oidc/profile/OIDCConstants.java | 12 +-
.../sp/consumer/oidc/code/query/query-beans.xml | 4 +-
.../idp/flows/sp/consumer/oidc/oidc-beans.xml | 120 +++++---
.../idp/flows/sp/consumer/oidc/oidc-flow.xml | 8 +-
.../idp/flows/sp/initiator/oidc/oidc-beans.xml | 323 +++++++++++----------
.../idp/flows/sp/initiator/oidc/oidc-flow.xml | 10 +-
.../shibboleth/idp/flows/sp/oidc-common-beans.xml | 51 ++++
.../oidc/flows/OIDCSessionInitiatorFlowTest.java | 7 +-
.../sp/oidc/flows/OIDCTokenConsumerFlowTest.java | 167 +++++++----
.../shibboleth/sp/oidc/flows/TestConstants.java | 126 +++++---
...DCEnvironmentApplicationContextInitializer.java | 4 +
.../impl/DecodeStateAsJsonObjectConsumer.java | 107 ++++++-
.../sp/oidc/profile/impl/IssueStateCookie.java | 7 +-
.../profile/impl/MapStateTokenToStateValue.java | 167 +++++++++++
...teToken.java => MapStateValueToStateToken.java} | 98 +++----
.../sp/oidc/profile/impl/OIDCSupport.java | 26 +-
...essStateCookie.java => ResolveStateCookie.java} | 24 +-
...AuthenticationRequestToPeerContextConsumer.java | 40 +--
.../impl/SetAuthenticationStateTokenConsumer.java | 50 ++++
...SetCorrelationCookieValueToContextConsumer.java | 184 ++++++++++++
.../impl/SetNonceValueToTokenContextConsumer.java | 53 ----
.../sp/oidc/profile/impl/StateLookupStrategy.java | 89 +++++-
.../oidc/profile/impl/ValidateResponseState.java | 68 +++--
.../sp/oidc/profile/impl/IssueStateCookieTest.java | 18 +-
...CookieTest.java => ResolveStateCookieTest.java} | 8 +-
...enticationRequestToPeerContextConsumerTest.java | 12 +-
34 files changed, 1548 insertions(+), 613 deletions(-)
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
index 4bdaf58..99fd52b 100644
--- 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
@@ -21,19 +21,42 @@ 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.
+ * A context to hold the token used to recover state information about the original authentication request, alongside
+ * any recovered information.
+ *
+ * <p>On initiation requests, it is likely only the token will be populated. In consumer flows, it is likely both the
+ * token and the recovered state is populated.</p>
*/
public class AuthnRequestStateDataContext extends BaseContext {
+ /** The token used as a key to the stored authentication request state.*/
+ @Nullable private String token;
+
/** The authentication state data. */
@Nullable private AuthenticationRequestStateData authnState;
/**
- * Sets the authentication state recovered from the authentication request.
+ * Set the token used as a key to the stored authentication request state.
+ *
+ * @param tokenIn the token to set.
+ */
+ @Nonnull public AuthnRequestStateDataContext setToken(@Nullable final String tokenIn) {
+ token = tokenIn;
+ return this;
+ }
+ /**
+ * Return the token used as a key to the stored authentication request state.
*
- * @param state authnentication state to set
+ * @return the token.
+ */
+ @Nullable public String getToken() {
+ return token;
+ }
+
+ /**
+ * Sets the authentication state recovered from the authentication request.
*
- * @return this context
+ * @param state The authnState to set
*/
@Nonnull public AuthnRequestStateDataContext setAuthnState(@Nullable final AuthenticationRequestStateData state) {
authnState = state;
@@ -49,4 +72,4 @@ public class AuthnRequestStateDataContext extends BaseContext {
return authnState;
}
-}
+}
\ No newline at end of file
diff --git a/sp-oidc-api/src/main/java/net/shibboleth/sp/oidc/context/CorrelationCookieStateContext.java b/sp-oidc-api/src/main/java/net/shibboleth/sp/oidc/context/CorrelationCookieStateContext.java
new file mode 100644
index 0000000..a206e07
--- /dev/null
+++ b/sp-oidc-api/src/main/java/net/shibboleth/sp/oidc/context/CorrelationCookieStateContext.java
@@ -0,0 +1,54 @@
+/*
+ * 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.minidev.json.JSONObject;
+
+/**
+ * A context to hold the browser bound correlation cookie referenced from the OAuth state. The cookie is assumed to hold
+ * a JSON object.
+ */
+public class CorrelationCookieStateContext extends BaseContext {
+
+ /** The correlation cookie value as a JSON object.*/
+ @Nullable private JSONObject value;
+
+ /**
+ * Set the correlation cookie value JSON.
+ *
+ * @param cookieValue the correlation cookie value JSON.
+ *
+ * @return this context
+ */
+ @Nonnull public CorrelationCookieStateContext setValue(@Nullable final JSONObject cookieValue) {
+ value = cookieValue;
+ return this;
+ }
+
+ /**
+ * Get the cookie value as a JSON object.
+ *
+ * @return the cookie value as a JSON object, or null if not set.
+ */
+ @Nullable public JSONObject getCorrelationCookieValue() {
+ return value;
+ }
+
+}
diff --git a/sp-oidc-api/src/main/java/net/shibboleth/sp/oidc/messaging/navigate/NonceFromOAuthStateLookupFunction.java b/sp-oidc-api/src/main/java/net/shibboleth/sp/oidc/messaging/navigate/AuthenticationStateFromCorrelationCookieLookupFunction.java
similarity index 57%
copy from sp-oidc-api/src/main/java/net/shibboleth/sp/oidc/messaging/navigate/NonceFromOAuthStateLookupFunction.java
copy to sp-oidc-api/src/main/java/net/shibboleth/sp/oidc/messaging/navigate/AuthenticationStateFromCorrelationCookieLookupFunction.java
index 7c87ce8..115d708 100644
--- a/sp-oidc-api/src/main/java/net/shibboleth/sp/oidc/messaging/navigate/NonceFromOAuthStateLookupFunction.java
+++ b/sp-oidc-api/src/main/java/net/shibboleth/sp/oidc/messaging/navigate/AuthenticationStateFromCorrelationCookieLookupFunction.java
@@ -20,29 +20,34 @@ import javax.annotation.Nullable;
import net.minidev.json.JSONObject;
import net.shibboleth.shared.primitive.StringSupport;
-import net.shibboleth.sp.oidc.context.OAuthStateContext;
+import net.shibboleth.sp.oidc.context.CorrelationCookieStateContext;
import net.shibboleth.sp.oidc.profile.OIDCConstants;
/**
- * A lookup function that extracts the resource state value from a Base64URL encoded JSON object.
+ * A function that extracts the authentication state token from the correlation cookie, if present, and returns it.
*/
-public class NonceFromOAuthStateLookupFunction implements Function<OAuthStateContext,String> {
+public class AuthenticationStateFromCorrelationCookieLookupFunction
+ implements Function<CorrelationCookieStateContext,String> {
/** {@inheritDoc} */
- @Nullable public String apply(@Nullable final OAuthStateContext stateContext) {
+ @Nullable public String apply(@Nullable final CorrelationCookieStateContext stateContext) {
if (stateContext == null) {
return null;
}
- final JSONObject stateJson = stateContext.getStateJson();
- if (stateJson == null) {
+ final JSONObject correlationCookie = stateContext.getCorrelationCookieValue();
+ if (correlationCookie == null) {
return null;
}
- final String nonceFromState = stateJson.getAsString(OIDCConstants.NONCE_FIELD);
- if (StringSupport.trimOrNull(nonceFromState) != null) {
- return nonceFromState;
+ final String authnStateToken = correlationCookie.getAsString(OIDCConstants.AUTHN_STATE_FIELD);
+ if (StringSupport.trimOrNull(authnStateToken) != null) {
+ return authnStateToken;
}
return null;
}
+
+
}
+
+
diff --git a/sp-oidc-api/src/main/java/net/shibboleth/sp/oidc/messaging/navigate/NonceFromAuthenticationRequestStateLookupFunction.java b/sp-oidc-api/src/main/java/net/shibboleth/sp/oidc/messaging/navigate/NonceFromAuthenticationRequestStateLookupFunction.java
deleted file mode 100644
index 97f345c..0000000
--- a/sp-oidc-api/src/main/java/net/shibboleth/sp/oidc/messaging/navigate/NonceFromAuthenticationRequestStateLookupFunction.java
+++ /dev/null
@@ -1,53 +0,0 @@
-/*
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package net.shibboleth.sp.oidc.messaging.navigate;
-
-import java.util.function.Function;
-
-import javax.annotation.Nonnull;
-import javax.annotation.Nullable;
-
-import org.slf4j.Logger;
-
-import net.minidev.json.JSONObject;
-import net.shibboleth.oidc.profile.core.OIDCAuthenticationRequest;
-import net.shibboleth.shared.primitive.LoggerFactory;
-import net.shibboleth.sp.oidc.profile.OIDCConstants;
-
-/**
- * A {@link Function} that returns a nonce found from the state if the state was encoded as a JSON object.
- */
-public class NonceFromAuthenticationRequestStateLookupFunction implements Function<OIDCAuthenticationRequest,String> {
-
- /** Class logger. */
- @Nonnull
- private final Logger log = LoggerFactory.getLogger(NonceFromAuthenticationRequestStateLookupFunction.class);
-
- /** {@inheritDoc} */
- @Nullable public String apply(@Nullable final OIDCAuthenticationRequest request) {
- if (request != null) {
- final JSONObject stateJson = request.getStateJson();
- if (stateJson != null) {
- final Object nonceFromOAuthStateAsObject = stateJson.get(OIDCConstants.NONCE_FIELD);
- if (nonceFromOAuthStateAsObject instanceof final String nonce) {
- return nonce;
- }
- }
- }
- return null;
- }
-
-}
-
diff --git a/sp-oidc-api/src/main/java/net/shibboleth/sp/oidc/messaging/navigate/NonceFromOAuthStateLookupFunction.java b/sp-oidc-api/src/main/java/net/shibboleth/sp/oidc/messaging/navigate/RFPFromCorrelationCookieLookupFunction.java
similarity index 58%
copy from sp-oidc-api/src/main/java/net/shibboleth/sp/oidc/messaging/navigate/NonceFromOAuthStateLookupFunction.java
copy to sp-oidc-api/src/main/java/net/shibboleth/sp/oidc/messaging/navigate/RFPFromCorrelationCookieLookupFunction.java
index 7c87ce8..6dde325 100644
--- a/sp-oidc-api/src/main/java/net/shibboleth/sp/oidc/messaging/navigate/NonceFromOAuthStateLookupFunction.java
+++ b/sp-oidc-api/src/main/java/net/shibboleth/sp/oidc/messaging/navigate/RFPFromCorrelationCookieLookupFunction.java
@@ -20,29 +20,34 @@ import javax.annotation.Nullable;
import net.minidev.json.JSONObject;
import net.shibboleth.shared.primitive.StringSupport;
-import net.shibboleth.sp.oidc.context.OAuthStateContext;
+import net.shibboleth.sp.oidc.context.CorrelationCookieStateContext;
import net.shibboleth.sp.oidc.profile.OIDCConstants;
/**
- * A lookup function that extracts the resource state value from a Base64URL encoded JSON object.
+ * A function that extracts the Request Forgery Protection nonce from the correlation cookie, if present, and
+ * returns it.
*/
-public class NonceFromOAuthStateLookupFunction implements Function<OAuthStateContext,String> {
+public class RFPFromCorrelationCookieLookupFunction implements Function<CorrelationCookieStateContext,String> {
/** {@inheritDoc} */
- @Nullable public String apply(@Nullable final OAuthStateContext stateContext) {
+ @Nullable public String apply(@Nullable final CorrelationCookieStateContext stateContext) {
if (stateContext == null) {
return null;
}
- final JSONObject stateJson = stateContext.getStateJson();
- if (stateJson == null) {
+ final JSONObject correlationCookie = stateContext.getCorrelationCookieValue();
+ if (correlationCookie == null) {
return null;
}
- final String nonceFromState = stateJson.getAsString(OIDCConstants.NONCE_FIELD);
- if (StringSupport.trimOrNull(nonceFromState) != null) {
- return nonceFromState;
+ final String rfpToken = correlationCookie.getAsString(OIDCConstants.RFP_FIELD);
+ if (StringSupport.trimOrNull(rfpToken) != null) {
+ return rfpToken;
}
return null;
}
+
+
}
+
+
diff --git a/sp-oidc-api/src/main/java/net/shibboleth/sp/oidc/messaging/navigate/NonceFromOAuthStateLookupFunction.java b/sp-oidc-api/src/main/java/net/shibboleth/sp/oidc/messaging/navigate/RFPFromOAuthStateLookupFunction.java
similarity index 86%
rename from sp-oidc-api/src/main/java/net/shibboleth/sp/oidc/messaging/navigate/NonceFromOAuthStateLookupFunction.java
rename to sp-oidc-api/src/main/java/net/shibboleth/sp/oidc/messaging/navigate/RFPFromOAuthStateLookupFunction.java
index 7c87ce8..2f22c2a 100644
--- a/sp-oidc-api/src/main/java/net/shibboleth/sp/oidc/messaging/navigate/NonceFromOAuthStateLookupFunction.java
+++ b/sp-oidc-api/src/main/java/net/shibboleth/sp/oidc/messaging/navigate/RFPFromOAuthStateLookupFunction.java
@@ -24,9 +24,9 @@ import net.shibboleth.sp.oidc.context.OAuthStateContext;
import net.shibboleth.sp.oidc.profile.OIDCConstants;
/**
- * A lookup function that extracts the resource state value from a Base64URL encoded JSON object.
+ * A lookup function that extracts the request forgery protection value from a Base64URL encoded JSON object.
*/
-public class NonceFromOAuthStateLookupFunction implements Function<OAuthStateContext,String> {
+public class RFPFromOAuthStateLookupFunction implements Function<OAuthStateContext,String> {
/** {@inheritDoc} */
@Nullable public String apply(@Nullable final OAuthStateContext stateContext) {
@@ -37,7 +37,7 @@ public class NonceFromOAuthStateLookupFunction implements Function<OAuthStateCon
if (stateJson == null) {
return null;
}
- final String nonceFromState = stateJson.getAsString(OIDCConstants.NONCE_FIELD);
+ final String nonceFromState = stateJson.getAsString(OIDCConstants.RFP_FIELD);
if (StringSupport.trimOrNull(nonceFromState) != null) {
return nonceFromState;
}
diff --git a/sp-oidc-api/src/main/java/net/shibboleth/sp/oidc/messaging/navigate/RequestCorrelationFromStateLookupFunction.java b/sp-oidc-api/src/main/java/net/shibboleth/sp/oidc/messaging/navigate/RequestCorrelationFromStateLookupFunction.java
new file mode 100644
index 0000000..8187070
--- /dev/null
+++ b/sp-oidc-api/src/main/java/net/shibboleth/sp/oidc/messaging/navigate/RequestCorrelationFromStateLookupFunction.java
@@ -0,0 +1,184 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.sp.oidc.messaging.navigate;
+
+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.nimbusds.jose.util.StandardCharset;
+
+import net.minidev.json.JSONObject;
+import net.minidev.json.JSONValue;
+import net.shibboleth.oidc.profile.core.OIDCAuthenticationRequest;
+import net.shibboleth.shared.annotation.ParameterName;
+import net.shibboleth.shared.codec.Base64Support;
+import net.shibboleth.shared.codec.EncodingException;
+import net.shibboleth.shared.component.AbstractIdentifiableInitializableComponent;
+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.context.AuthnRequestStateDataContext;
+import net.shibboleth.sp.oidc.profile.OIDCConstants;
+
+/**
+ * A {@link Function} that creates a request correlation cookie value with the token/key of the stored authentication
+ * request state, and the request forgery protection (RFP) value that was stored in the OAuth state parameter.
+ * The resulting cookie value is Base64URL encoded JSON string (noting the sealer will already base64 encode the
+ * sealed String).
+ */
+public class RequestCorrelationFromStateLookupFunction extends AbstractIdentifiableInitializableComponent
+ implements Function<ProfileRequestContext,String> {
+
+ /** Class logger. */
+ @Nonnull
+ private final Logger log = LoggerFactory.getLogger(RequestCorrelationFromStateLookupFunction.class);
+
+ /** A strategy to locate the {@link AuthnRequestStateDataContext}.*/
+ @Nonnull
+ private final Function<ProfileRequestContext, AuthnRequestStateDataContext> authnRequestDataStateLookupStrategy;
+
+ /** Strategy to locate the authentication request. */
+ @Nonnull
+ private final 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.
+ *
+ * @param dataLookuptrategy a strategy to locate the {@link AuthnRequestStateDataContext}
+ * @param authnRequestStrategy a strategy to locate the {@link OIDCAuthenticationRequest}
+ */
+ public RequestCorrelationFromStateLookupFunction(@ParameterName(name="authenticationRequestDataStateLookupStrategy")
+ @Nonnull final Function<ProfileRequestContext, AuthnRequestStateDataContext> dataLookuptrategy,
+ @ParameterName(name="authenticationRequestLookupStrategy")
+ @Nonnull final Function<ProfileRequestContext, OIDCAuthenticationRequest> authnRequestStrategy) {
+
+ authnRequestDataStateLookupStrategy =
+ Constraint.isNotNull(dataLookuptrategy, "AuthnRequestStateDataContext lookup strategy cannot be null");
+ authenticationRequestLookupStrategy =
+ Constraint.isNotNull(authnRequestStrategy, "OIDCAuthenticationRequest lookup strategy cannot be null");
+ sealState = PredicateSupport.alwaysTrue();
+ }
+
+
+ /**
+ * 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();
+ }
+
+ /** {@inheritDoc} */
+ @Nullable public String apply(@Nullable final ProfileRequestContext prc) {
+ final AuthnRequestStateDataContext stateContext = authnRequestDataStateLookupStrategy.apply(prc);
+
+ if (stateContext == null || stateContext.getToken() == null) {
+ log.error("No authentication request state found, can not create correlation cookie");
+ return null;
+ }
+
+ final OIDCAuthenticationRequest authnRequest = authenticationRequestLookupStrategy.apply(prc);
+
+ String csrfNonce = null;
+ if (authnRequest != null) {
+ final JSONObject stateJson = authnRequest.getStateJson();
+ if (stateJson != null) {
+ final Object nonceFromOAuthStateAsObject = stateJson.get(OIDCConstants.RFP_FIELD);
+ if (nonceFromOAuthStateAsObject instanceof final String nonce) {
+ csrfNonce = nonce;
+ }
+ }
+ }
+ if (csrfNonce == null) {
+ log.error("Unable to generate request correlation cookie, no request forgery protection value found");
+ return null;
+ }
+
+ final JSONObject stateObject = new JSONObject();
+ stateObject.appendField(OIDCConstants.AUTHN_STATE_FIELD, stateContext.getToken());
+ stateObject.appendField(OIDCConstants.RFP_FIELD, csrfNonce);
+ final String stateJsonString = stateObject.toJSONString(JSONValue.COMPRESSION);
+ if (stateJsonString == null || stateJsonString.isEmpty()) {
+ return null;
+ }
+ try {
+ final DataSealer localDataSealer = dataSealer;
+ if (localDataSealer != null && sealState.test(prc)) {
+ log.debug("{}: Request correlation cookie is sealed", getId());
+ final String sealed = localDataSealer.wrap(stateJsonString);
+ final byte[] sealedAsBytes = sealed.getBytes(StandardCharset.UTF_8);
+ assert sealedAsBytes != null;
+ return Base64Support.encodeURLSafe(sealedAsBytes);
+ } else {
+ log.warn("{}: Request correlation cookie was NOT sealed, either DataSealer is not configured or "
+ + "sealing predicate returned false. Sealing should be enabled in production", getId());
+ final byte[] stateJsonAsBytes = stateJsonString.getBytes(StandardCharset.UTF_8);
+ assert stateJsonAsBytes != null;
+ return Base64Support.encodeURLSafe(stateJsonAsBytes);
+ }
+
+ } catch (final EncodingException | DataSealerException e) {
+ log.error("Unable to generate request correlation cookie", e);
+ return null;
+ }
+
+ }
+
+}
+
diff --git a/sp-oidc-api/src/main/java/net/shibboleth/sp/oidc/messaging/navigate/ResourceStateFromJSONStateLookupFunction.java b/sp-oidc-api/src/main/java/net/shibboleth/sp/oidc/messaging/navigate/StateFromJSONStateLookupFunction.java
similarity index 94%
rename from sp-oidc-api/src/main/java/net/shibboleth/sp/oidc/messaging/navigate/ResourceStateFromJSONStateLookupFunction.java
rename to sp-oidc-api/src/main/java/net/shibboleth/sp/oidc/messaging/navigate/StateFromJSONStateLookupFunction.java
index f32b992..b79528e 100644
--- a/sp-oidc-api/src/main/java/net/shibboleth/sp/oidc/messaging/navigate/ResourceStateFromJSONStateLookupFunction.java
+++ b/sp-oidc-api/src/main/java/net/shibboleth/sp/oidc/messaging/navigate/StateFromJSONStateLookupFunction.java
@@ -27,7 +27,7 @@ import net.shibboleth.sp.oidc.profile.OIDCConstants;
* Lookup strategy that extracts the 'state' parameter from the OAuth 2.0 state JSON Object in the state context. This
* state is used to carry a reference to the resource URL.
*/
-public class ResourceStateFromJSONStateLookupFunction implements Function<OAuthStateContext,String> {
+public class StateFromJSONStateLookupFunction implements Function<OAuthStateContext,String> {
/** {@inheritDoc} */
@Nullable public String apply(@Nullable final OAuthStateContext stateContext) {
diff --git a/sp-oidc-api/src/main/java/net/shibboleth/sp/oidc/profile/OIDCConstants.java b/sp-oidc-api/src/main/java/net/shibboleth/sp/oidc/profile/OIDCConstants.java
index 934a69b..20dc9c9 100644
--- a/sp-oidc-api/src/main/java/net/shibboleth/sp/oidc/profile/OIDCConstants.java
+++ b/sp-oidc-api/src/main/java/net/shibboleth/sp/oidc/profile/OIDCConstants.java
@@ -28,10 +28,16 @@ public final class OIDCConstants {
}
- /** Name of the state field to add to the state JSON object.*/
+ /**
+ * Name of the state field to add to the state JSON object. The state field represents the current 'state'
+ * value of the DDF.
+ */
@Nonnull @NotEmpty public static final String STATE_FIELD = "state";
- /** Name of the nonce field to add to the state JSON object.*/
- @Nonnull @NotEmpty public static final String NONCE_FIELD = "nonce";
+ /** Name of the authentication state token field that references the authentication state in the token manager.*/
+ @Nonnull @NotEmpty public static final String AUTHN_STATE_FIELD = "authnState";
+
+ /** Name of the Request Forgery Protection (nonce) field.*/
+ @Nonnull @NotEmpty public static final String RFP_FIELD = "rfp";
}
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 3a1ba1d..3983c6d 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
@@ -15,7 +15,9 @@
p:postDecodeStrategyFailureIsError="true"
p:httpServletRequestSupplier-ref="shibboleth.RemotedHttpServletRequestSupplier">
<property name="postDecodeStrategy">
- <bean class="net.shibboleth.sp.oidc.profile.decoding.impl.DecodeStateAsJsonObjectConsumer"/>
+ <bean class="net.shibboleth.sp.oidc.profile.decoding.impl.DecodeStateAsJsonObjectConsumer"
+ p:dataSealer-ref="#{'%{sp.dataSealer:shibboleth.DataSealer}'.trim()}"
+ p:unsealStatePredicate="#{getObject('%{sp.oidc.sealOAuthStatePredicate:}') ?: ((%{sp.oidc.sealOAuthState:true}) ? getObject('shibboleth.Conditions.TRUE') : getObject('shibboleth.Conditions.FALSE'))}"/>
</property>
</bean>
</constructor-arg>
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 fb8a000..3b89cb7 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
@@ -16,20 +16,43 @@
<!--
- The State parameter can carry target URL like SAML relayState, but it must also be used to prevent CSRF in OAuth.
- So we can attempt to map it from the state parameter.
+ Map the state reference from the OAuth state parameter to the stored off resource URL.
-->
<bean id="MapStateTokenToResource"
class="net.shibboleth.sp.profile.impl.MapStateTokenToResource" scope="prototype"
p:stateTokenLookupStrategy-ref="StateFromStateLookup"
p:createOutputObjects="true"/>
-
- <!-- Retrieve the state used to map to cookie values, taken from the JSON object contained in the OAuth 2.0 state parameter -->
+
+ <!--
+ Retrieve the correlation cookie from the state token (reference) in the OAuth state parameter. The cookie is used
+ to protect against request forgery, relate response to request, and contains the authn state token used to recover
+ authentication state.
+
+ Store the cookie value into the context for later inspection.
+ -->
+ <bean id="ProcessCorrelationCookie" class="net.shibboleth.sp.oidc.profile.impl.ResolveStateCookie" scope="prototype"
+ 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.SetCorrelationCookieValueToContextConsumer"
+ p:correlationCookieStateContextLookupStrategy-ref="shibboleth.ChildLookupOrCreate.CorrelationCookieStateContextFromInboundOAuthStateContext"
+ p:dataSealer-ref="#{'%{sp.dataSealer:shibboleth.DataSealer}'.trim()}"
+ p:unsealStatePredicate="#{getObject('%{sp.oidc.sealCorrelationCookiePredicate:}') ?: ((%{sp.oidc.sealCorrelationCookie:true}) ? getObject('shibboleth.Conditions.TRUE') : getObject('shibboleth.Conditions.FALSE'))}"/>
+ </property>
+ </bean>
+
+ <!--
+ Retrieve the state token from the the OAuth 2.0 state parameter which is used to map to the correlation cookie
+ and the resource URL. The state parameter value will be decoded into the OAuth state context by this point.
+ -->
<bean id="StateFromStateLookup" parent="shibboleth.Functions.Compose">
<constructor-arg name="g">
<bean parent="shibboleth.Functions.Compose">
<constructor-arg name="g">
- <bean class="net.shibboleth.sp.oidc.messaging.navigate.ResourceStateFromJSONStateLookupFunction"/>
+ <bean class="net.shibboleth.sp.oidc.messaging.navigate.StateFromJSONStateLookupFunction"/>
</constructor-arg>
<constructor-arg name="f">
<bean class="org.opensaml.messaging.context.navigate.ChildContextLookup"
@@ -40,26 +63,13 @@
<constructor-arg name="f" ref="shibboleth.MessageContextLookup.Inbound"/>
</bean>
- <!-- Correlation cookie is used to -->
- <bean id="ProcessNonceCorrelationCookie" class="net.shibboleth.sp.oidc.profile.impl.ProcessStateCookie" scope="prototype"
- 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>
-
- <!-- TODO should this be a cookie or go through the state manager-->
- <bean id="ProcessAuthnStateCookie" class="net.shibboleth.sp.oidc.profile.impl.ProcessStateCookie" scope="prototype"
- p:cookieManager-ref="shibboleth.sp.RemotedCookieManager"
- p:cookiePrefix="%{sp.authnstate.cookiePrefix:__Host-shibsp_authnstate_}"
- p:createOutputObjects="true"
+ <!--
+ Retrieve the stored authentication state/value from the storage manager using the authn state token in the correlation cookie
+ -->
+ <bean id="RecoverAuthenticationState" class="net.shibboleth.sp.oidc.profile.impl.MapStateTokenToStateValue" scope="prototype"
p:errorFatal="true"
- p:stateTokenLookupStrategy-ref="StateFromStateLookup">
- <property name="cookieValueConsumerStrategy">
+ p:stateTokenLookupStrategy-ref="AuthenticationStateFromCorrelationCookieLookup">
+ <property name="stateValueConsumer">
<bean class="net.shibboleth.sp.oidc.profile.impl.SetAuthenticationRequestToPeerContextConsumer"
p:objectMapper-ref="shibboleth.JSONObjectMapper"
p:dataSealer-ref="#{'%{sp.dataSealer:shibboleth.DataSealer}'.trim()}"
@@ -68,28 +78,45 @@
</property>
</bean>
+ <bean id="AuthenticationStateFromCorrelationCookieLookup" parent="shibboleth.Functions.Compose">
+ <constructor-arg name="g">
+ <bean parent="shibboleth.Functions.Compose">
+ <constructor-arg name="g">
+ <bean class="net.shibboleth.sp.oidc.messaging.navigate.AuthenticationStateFromCorrelationCookieLookupFunction"/>
+ </constructor-arg>
+ <constructor-arg name="f" ref="shibboleth.ChildLookup.CorrelationCookieStateContextFromOAuthStateContext"/>
+ </bean>
+ </constructor-arg>
+ <constructor-arg name="f" ref="shibboleth.MessageContextLookup.Inbound"/>
+ </bean>
+
<bean id="ValidateAuthenticationResponseResult" scope="prototype"
class="net.shibboleth.sp.oidc.profile.impl.ValidateAuthenticationResponseResult" />
<bean id="ValidateResponseStateMatchesRequest" scope="prototype"
class="net.shibboleth.sp.oidc.profile.impl.ValidateResponseState"
- p:nonceTokenLookupStrategy-ref="NonceFromStateLookup" />
-
- <bean id="InitializeRelyingPartyContextFromOIDCPeer"
- class="net.shibboleth.sp.oidc.profile.impl.InitializeRelyingPartyContextFromOIDCPeer" scope="prototype" />
+ p:rfpTokenFromOAuthStateLookupStrategy-ref="RFPFromOAuthStateLookup"
+ p:rfpFromCookieLookupStrategy-ref="RFPFromCorrelationCookieLookup"/>
- <bean id="SelectRelyingPartyConfiguration"
- class="net.shibboleth.sp.profile.impl.SelectRelyingPartyConfiguration" scope="prototype" />
+ <!-- Get the Request Forgery Protection value from the correlation cookie -->
+ <bean id="RFPFromCorrelationCookieLookup" parent="shibboleth.Functions.Compose">
+ <constructor-arg name="g">
+ <bean parent="shibboleth.Functions.Compose">
+ <constructor-arg name="g">
+ <bean class="net.shibboleth.sp.oidc.messaging.navigate.RFPFromCorrelationCookieLookupFunction"/>
+ </constructor-arg>
+ <constructor-arg name="f" ref="shibboleth.ChildLookup.CorrelationCookieStateContextFromOAuthStateContext"/>
+ </bean>
+ </constructor-arg>
+ <constructor-arg name="f" ref="shibboleth.MessageContextLookup.Inbound"/>
+ </bean>
- <bean id="SelectProfileConfiguration"
- class="net.shibboleth.idp.profile.impl.SelectProfileConfiguration" scope="prototype"
- p:profileId-ref="shibboleth.sp.oidc.ProfileId" />
-
- <bean id="NonceFromStateLookup" parent="shibboleth.Functions.Compose">
+ <!-- Get the Request Forgery Protection value from the OAuth 2.0 state parameter -->
+ <bean id="RFPFromOAuthStateLookup" parent="shibboleth.Functions.Compose">
<constructor-arg name="g">
<bean parent="shibboleth.Functions.Compose">
<constructor-arg name="g">
- <bean class="net.shibboleth.sp.oidc.messaging.navigate.NonceFromOAuthStateLookupFunction"/>
+ <bean class="net.shibboleth.sp.oidc.messaging.navigate.RFPFromOAuthStateLookupFunction"/>
</constructor-arg>
<constructor-arg name="f">
<bean class="org.opensaml.messaging.context.navigate.ChildContextLookup"
@@ -99,8 +126,18 @@
</constructor-arg>
<constructor-arg name="f" ref="shibboleth.MessageContextLookup.Inbound"/>
</bean>
+
+ <bean id="InitializeRelyingPartyContextFromOIDCPeer"
+ class="net.shibboleth.sp.oidc.profile.impl.InitializeRelyingPartyContextFromOIDCPeer" scope="prototype" />
+
+ <bean id="SelectRelyingPartyConfiguration"
+ class="net.shibboleth.sp.profile.impl.SelectRelyingPartyConfiguration" scope="prototype" />
+
+ <bean id="SelectProfileConfiguration"
+ class="net.shibboleth.idp.profile.impl.SelectProfileConfiguration" scope="prototype"
+ p:profileId-ref="shibboleth.sp.oidc.ProfileId" />
- <!-- Build the request based on the inbound context -->
+ <!-- Build the Token endpoint client authentication method based on the inbound context -->
<bean id="InitializeOAuth2ClientAuthenticationContextHandler" parent="WebFlowInboundMessageHandlerAdaptor"
scope="prototype">
<constructor-arg>
@@ -194,7 +231,6 @@
</property>
</bean>
- <!-- ID TOKEN Signature Validation -->
<bean id="IDTokenSignatureValidation" parent="WebFlowInboundMessageHandlerAdaptor" scope="prototype">
<constructor-arg>
<bean class="org.opensaml.messaging.handler.impl.BasicMessageHandlerChain">
@@ -409,7 +445,7 @@
class="net.shibboleth.sp.oidc.profile.impl.RequestedACRValidationActivationCondition"
c:relyingPartyContextLookupStrategy-ref="shibboleth.ChildLookup.RelyingParty"/>
- <!-- - End ID Token Claims Validation -->
+ <!-- End ID Token Claims Validation -->
<bean id="CheckUserInfoRequiredCondition" class=" net.shibboleth.oidc.profile.config.logic.UserInfoLookupPredicate" />
@@ -468,9 +504,9 @@
</bean>
<!--
- Note, this is identical in setup to the id_token signature validation flow as they both use the same config and trust engine.
- the only difference is the location of the JWT to validate. Maybe they could be merged. Also, the populate steps may or may not
- have already been performed in the id_token validation depending on the activation condition, so maybe those could be consolidated.
+ Note, this is identical in setup to the id_token signature validation flow as they both use the same config and trust engine.
+ the only difference is the location of the JWT to validate. Maybe they could be merged. Also, the populate steps may or may not
+ have already been performed in the id_token validation depending on the activation condition, so maybe those could be consolidated.
-->
<bean id="UserInfoTokenSignatureValidation" parent="WebFlowInboundMessageHandlerAdaptor" scope="prototype">
<constructor-arg>
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 defda79..024b1d0 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
@@ -6,11 +6,11 @@
<action-state id="DoProfileWork">
<!-- <evaluate expression="PopulateMetricContext" />-->
<evaluate expression="DecodeMessage" />
+ <evaluate expression="ValidateAuthenticationResponseResult" /> <!-- TODO: If this fails the state below is not cleared! -->
<evaluate expression="MapStateTokenToResource" />
- <evaluate expression="ProcessNonceCorrelationCookie" />
- <evaluate expression="ProcessAuthnStateCookie" />
- <evaluate expression="ValidateResponseStateMatchesRequest" />
- <evaluate expression="ValidateAuthenticationResponseResult" />
+ <evaluate expression="ProcessCorrelationCookie"/>
+ <evaluate expression="RecoverAuthenticationState" />
+ <evaluate expression="ValidateResponseStateMatchesRequest" />
<evaluate expression="ProviderMetadataLookup" />
<evaluate expression="InitializeRelyingPartyContextFromOIDCPeer" />
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 b9aa165..58d70d2 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
@@ -6,56 +6,54 @@
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">
-
+
<import resource="classpath:/META-INF/net/shibboleth/idp/flows/sp/oidc-common-beans.xml" />
-
+
<!-- TODO Check we need the thread-local variants of these in the chains we are using them ^^ -->
-
+
<util:constant id="shibboleth.sp.oidc.ProfileId"
static-field="net.shibboleth.oidc.profile.config.OIDCSSOProfileConfiguration.PROFILE_ID" />
<!-- end -->
-
-
+
+
<bean id="ValidateSessionInitiatorRequest"
- class="net.shibboleth.sp.profile.impl.ValidateSessionInitiatorRequest" scope="prototype"
+ class="net.shibboleth.sp.profile.impl.ValidateSessionInitiatorRequest"
+ scope="prototype"
p:flowId="oidc"
- p:requireDiscoveryURL="false"
+ p:requireDiscoveryURL="false"
p:requireRelyingPartyId="true" />
-
- <!-- Prepare the OIDC Peer Entity with the relying party ID (the OP identifier)-->
- <bean id="PrepareInboundMessageContext"
+
+ <!-- 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:relyingPartyLookupStrategy-ref="shibboleth.RelyingPartyIdLookup.Simple">
</bean>
-
+
<bean id="InitializeRelyingPartyContextFromOIDCPeer"
class="net.shibboleth.sp.oidc.profile.impl.InitializeRelyingPartyContextFromOIDCPeer" scope="prototype" />
-
+
<bean id="SelectRelyingPartyConfiguration"
class="net.shibboleth.sp.profile.impl.SelectRelyingPartyConfiguration" scope="prototype" />
-
- <bean id="SelectProfileConfiguration"
- class="net.shibboleth.idp.profile.impl.SelectProfileConfiguration" scope="prototype"
- p:profileId-ref="shibboleth.sp.oidc.ProfileId" />
-
-<!-- <bean id="InitializeOutboundMessageContext"
- class="net.shibboleth.idp.saml.profile.impl.InitializeOutboundMessageContext" scope="prototype"
- p:selfIdentityLookupStrategy-ref="shibboleth.IssuerLookup.Simple" /> -->
-
- <!-- TODO, self context -->
- <bean id="InitializeOutboundMessageContext"
- class="net.shibboleth.sp.oidc.profile.impl.InitializeOutboundMessageContext"
- scope="prototype" />
+
+ <bean id="SelectProfileConfiguration" class="net.shibboleth.idp.profile.impl.SelectProfileConfiguration"
+ scope="prototype" p:profileId-ref="shibboleth.sp.oidc.ProfileId" />
+
+ <!-- <bean id="InitializeOutboundMessageContext" class="net.shibboleth.idp.saml.profile.impl.InitializeOutboundMessageContext"
+ scope="prototype" p:selfIdentityLookupStrategy-ref="shibboleth.IssuerLookup.Simple" /> -->
+
+ <!-- TODO, self context -->
+ <bean id="InitializeOutboundMessageContext"
+ class="net.shibboleth.sp.oidc.profile.impl.InitializeOutboundMessageContext" scope="prototype" />
<bean id="InitializeOAuth2ClientContext" scope="prototype"
class="net.shibboleth.sp.oidc.profile.impl.InitializeOAuth2ClientContext"
- p:issuerLookupStrategy-ref="shibboleth.ClientIdLookup.Simple"/>
-
+ p:issuerLookupStrategy-ref="shibboleth.ClientIdLookup.Simple" />
+
<bean id="InitializeAuthorizationRequest"
- class="net.shibboleth.sp.oidc.profile.impl.InitializeAuthorizationRequest" scope="prototype"/>
-
+ class="net.shibboleth.sp.oidc.profile.impl.InitializeAuthorizationRequest" scope="prototype" />
+
<!-- Construct a suitable outbound authentication request -->
-
+
<bean id="BuildAuthenticationRequest" parent="WebFlowOutboundMessageHandlerAdaptor" scope="prototype">
<constructor-arg>
<bean class="org.opensaml.messaging.handler.impl.BasicMessageHandlerChain">
@@ -65,98 +63,115 @@
<bean id="AddResponseType" scope="prototype"
class="net.shibboleth.oidc.profile.messaging.handler.impl.AddResponseTypeHandler">
<property name="parameterValueLookupStrategy">
- <bean class="net.shibboleth.sp.oidc.profile.impl.ResponseTypeLookupStrategy" scope="prototype"/>
+ <bean class="net.shibboleth.sp.oidc.profile.impl.ResponseTypeLookupStrategy"
+ scope="prototype" />
</property>
- </bean>
+ </bean>
<bean id="AddResponseMode" scope="prototype"
class="net.shibboleth.oidc.profile.messaging.handler.impl.AddResponseModeHandler">
<property name="parameterValueLookupStrategy">
- <bean class="net.shibboleth.sp.oidc.profile.impl.ResponseModeLookupStrategy" scope="prototype"/>
+ <bean class="net.shibboleth.sp.oidc.profile.impl.ResponseModeLookupStrategy"
+ scope="prototype" />
</property>
- </bean>
-
+ </bean>
+
<bean id="AddMaxAge" scope="prototype"
class="net.shibboleth.oidc.profile.messaging.handler.impl.AddMaxAgeHandler">
<property name="parameterValueLookupStrategy">
- <bean class="net.shibboleth.sp.oidc.profile.impl.MaxAgeLookupStrategy" scope="prototype"/>
+ <bean class="net.shibboleth.sp.oidc.profile.impl.MaxAgeLookupStrategy"
+ scope="prototype" />
</property>
- </bean>
+ </bean>
<bean id="AddDisplay" scope="prototype"
- class="net.shibboleth.oidc.profile.messaging.handler.impl.AddDisplayHandler" >
+ class="net.shibboleth.oidc.profile.messaging.handler.impl.AddDisplayHandler">
<property name="parameterValueLookupStrategy">
- <bean class="net.shibboleth.sp.oidc.profile.impl.DisplayParameterLookupStrategy" scope="prototype"/>
+ <bean class="net.shibboleth.sp.oidc.profile.impl.DisplayParameterLookupStrategy"
+ scope="prototype" />
</property>
</bean>
<bean id="AddScopes" scope="prototype"
class="net.shibboleth.oidc.profile.messaging.handler.impl.AddScopesHandler">
<property name="parameterValueLookupStrategy">
- <bean class="net.shibboleth.sp.oidc.profile.impl.ScopeLookupStrategy" scope="prototype"/>
+ <bean class="net.shibboleth.sp.oidc.profile.impl.ScopeLookupStrategy"
+ scope="prototype" />
</property>
</bean>
<bean id="AddNonce" scope="prototype"
class="net.shibboleth.oidc.profile.messaging.handler.impl.AddNonceHandler">
<property name="parameterValueLookupStrategy">
- <bean class="net.shibboleth.sp.oidc.profile.impl.NonceLookupStrategy" scope="prototype"/>
+ <bean class="net.shibboleth.sp.oidc.profile.impl.NonceLookupStrategy"
+ scope="prototype" />
</property>
</bean>
<bean id="AddEndpointURI" scope="prototype"
class="net.shibboleth.oidc.profile.messaging.handler.impl.AddEndpointURIHandler">
<property name="parameterValueLookupStrategy">
- <bean class="net.shibboleth.sp.oidc.profile.impl.AuthorizationEndpointLookupStrategy" scope="prototype"/>
+ <bean
+ class="net.shibboleth.sp.oidc.profile.impl.AuthorizationEndpointLookupStrategy"
+ scope="prototype" />
</property>
</bean>
<bean id="AddLoginHintHandler" scope="prototype"
class="net.shibboleth.oidc.profile.messaging.handler.impl.AddLoginHintHandler">
<property name="parameterValueLookupStrategy">
- <bean class="net.shibboleth.sp.oidc.profile.impl.LoginHintLookupStrategy" scope="prototype"/>
+ <bean class="net.shibboleth.sp.oidc.profile.impl.LoginHintLookupStrategy"
+ scope="prototype" />
</property>
</bean>
<bean id="AddRequestedClaims" scope="prototype"
class="net.shibboleth.oidc.profile.messaging.handler.impl.AddRequestedClaimsHandler">
<property name="parameterValueLookupStrategy">
- <bean class="net.shibboleth.sp.oidc.profile.impl.RequestedClaimsLookupStrategy" scope="prototype"/>
- </property>
+ <bean class="net.shibboleth.sp.oidc.profile.impl.RequestedClaimsLookupStrategy"
+ scope="prototype" />
+ </property>
</bean>
<bean id="AddPKCECodeVerifierAndChallenge" scope="prototype"
class="net.shibboleth.oidc.profile.messaging.handler.impl.AddPKCECodeVerifierAndChallenge">
- <property name="parameterValueLookupStrategy">
- <bean class="net.shibboleth.sp.oidc.profile.impl.PKCEOptionsLookupStrategy" scope="prototype"/>
+ <property name="parameterValueLookupStrategy">
+ <bean class="net.shibboleth.sp.oidc.profile.impl.PKCEOptionsLookupStrategy"
+ scope="prototype" />
</property>
- </bean>
+ </bean>
<bean id="AddRedirectURI" scope="prototype"
class="net.shibboleth.oidc.profile.messaging.handler.impl.AddRedirectURIHandler">
<property name="parameterValueLookupStrategy">
- <bean class="net.shibboleth.sp.oidc.profile.impl.RedirectUriLookupStrategy" scope="prototype" />
- </property>
+ <bean class="net.shibboleth.sp.oidc.profile.impl.RedirectUriLookupStrategy"
+ scope="prototype" />
+ </property>
</bean>
<bean id="AddAuthenticationContextClassReferences" scope="prototype"
class="net.shibboleth.oidc.profile.messaging.handler.impl.AddAuthenticationContextClassReferencesHandler">
<property name="parameterValueLookupStrategy">
- <bean class="net.shibboleth.sp.oidc.profile.impl.AuthenticationContextClassRefLookupStrategy" scope="prototype" />
- </property>
+ <bean
+ class="net.shibboleth.sp.oidc.profile.impl.AuthenticationContextClassRefLookupStrategy"
+ scope="prototype" />
+ </property>
</bean>
<bean id="AddForceAuthentication" scope="prototype"
class="net.shibboleth.oidc.profile.messaging.handler.impl.AddForceAuthenticationHandler">
<property name="ParameterValueLookupStrategy">
- <bean class="net.shibboleth.sp.oidc.profile.impl.ForceAuthnParameterLookupStrategy"/>
+ <bean class="net.shibboleth.sp.oidc.profile.impl.ForceAuthnParameterLookupStrategy" />
</property>
</bean>
<bean id="AddPrompt" scope="prototype"
class="net.shibboleth.oidc.profile.messaging.handler.impl.AddPromptHandler">
<property name="parameterValueLookupStrategy">
- <bean class="net.shibboleth.sp.oidc.profile.impl.PromptLookupStrategy" scope="prototype"/>
+ <bean class="net.shibboleth.sp.oidc.profile.impl.PromptLookupStrategy"
+ scope="prototype" />
</property>
</bean>
<bean id="AddResourceIndicators" scope="prototype"
class="net.shibboleth.oidc.profile.messaging.handler.impl.AddResourceHandler">
<property name="parameterValueLookupStrategy">
- <bean class="net.shibboleth.sp.oidc.profile.impl.ResourceLookupStrategy" scope="prototype"/>
+ <bean class="net.shibboleth.sp.oidc.profile.impl.ResourceLookupStrategy"
+ scope="prototype" />
</property>
</bean>
<bean id="AddUiLocales" scope="prototype"
class="net.shibboleth.oidc.profile.messaging.handler.impl.AddUiLocalesHandler">
<property name="parameterValueLookupStrategy">
- <bean class="net.shibboleth.sp.oidc.profile.impl.UiLocalesLookupStrategy" scope="prototype"/>
+ <bean class="net.shibboleth.sp.oidc.profile.impl.UiLocalesLookupStrategy"
+ scope="prototype" />
</property>
</bean>
</list>
@@ -166,40 +181,40 @@
<property name="errorEvent">
<util:constant static-field="org.opensaml.profile.action.EventIds.INVALID_MESSAGE" />
</property>
- </bean>
-
+ </bean>
+
<bean id="RequestObjectRequiredAndSupportedPredicate" scope="prototype"
- class="net.shibboleth.sp.oidc.config.logic.RequestObjectRequiredAndSupported" />
-
+ class="net.shibboleth.sp.oidc.config.logic.RequestObjectRequiredAndSupported" />
+
<bean id="PopulateRequestObjectSignatureSigningParameters" scope="prototype"
class="net.shibboleth.oidc.profile.impl.PopulateJWTSignatureSigningParameters"
c:strategy-ref="shibboleth.MessageContextLookup.Inbound" p:noResultIsError="true"
p:configurationLookupStrategy-ref="RequestObjectSignatureSigningConfigurationLookup"
p:providerMetadataContextLookupStrategy-ref="shibboleth.ChildLookup.OIDCProviderMetadataContextFromOutbound"
p:signatureSigningParametersResolver-ref="RequestObjectSignatureSigningParametersResolver">
- <property name="activationCondition">
- <bean id="SignRequestObjectProxyCondition"
- class="net.shibboleth.oidc.profile.config.logic.SignRequestObjectPredicate"
- p:relyingPartyContextLookupStrategy-ref="shibboleth.ChildLookup.RelyingParty" />
- </property>
+ <property name="activationCondition">
+ <bean id="SignRequestObjectProxyCondition"
+ class="net.shibboleth.oidc.profile.config.logic.SignRequestObjectPredicate"
+ p:relyingPartyContextLookupStrategy-ref="shibboleth.ChildLookup.RelyingParty" />
+ </property>
</bean>
-
+
<bean id="RequestObjectSignatureSigningParametersResolver" scope="prototype"
class="net.shibboleth.oidc.security.jose.impl.RelyingPartySigningParametersResolver"
p:providerMetadataAlgorithmLookupStrategy-ref="RequestObjectSupportedSignatureSigningAlgorithms" />
-
+
<bean id="RequestObjectSupportedSignatureSigningAlgorithms" scope="prototype"
class="net.shibboleth.sp.oidc.metadata.impl.RequestObjectSupportedSignatureSigningAlgorithms" />
-
-
+
+
<bean id="RequestObjectSignatureSigningConfigurationLookup" lazy-init="true" scope="prototype"
class="net.shibboleth.oidc.profile.config.navigate.JWTSignatureSigningConfigurationLookupFunction" />
-
-
+
+
<!-- if the activation condition succeeds, encryption is not optional -->
<bean id="PopulateRequestObjectEncryptionParameters"
class="net.shibboleth.oidc.profile.impl.PopulateJWTEncryptionParameters" scope="prototype"
- p:encryptionOptional="false"
+ p:encryptionOptional="false"
p:forFriendlyName="Request Object"
p:configurationLookupStrategy-ref="RequestObjectEncryptionConfigurationLookup"
p:providerMetadataContextLookupStrategy-ref="shibboleth.ChildLookup.OIDCProviderMetadataContextFromOutbound"
@@ -211,7 +226,7 @@
</property>
</bean>
-
+
<bean id="EncryptionParametersResolver" scope="prototype"
class="net.shibboleth.oidc.security.jose.impl.DefaultEncryptionParametersResolver">
<property name="keyTransportEncryptionAlgorithmsLookupStrategy">
@@ -219,46 +234,67 @@
class="net.shibboleth.oidc.security.jose.impl.ProviderMetadataKeyTransportEncryptionAlgorithmsLookupStrategy">
<constructor-arg>
<bean
- class="net.shibboleth.oidc.profile.config.navigate.ProviderMetadataStringValuesLookupFunction"
- c:keyName="request_object_encryption_alg_values_supported"/>
+ class="net.shibboleth.oidc.profile.config.navigate.ProviderMetadataStringValuesLookupFunction"
+ c:keyName="request_object_encryption_alg_values_supported" />
</constructor-arg>
</bean>
</property>
<property name="dataEncryptionAlgorithmsLookupStrategy">
- <bean class="net.shibboleth.oidc.security.jose.impl.ProviderMetadataDataEncryptionAlgorithmsLookupStrategy">
+ <bean
+ class="net.shibboleth.oidc.security.jose.impl.ProviderMetadataDataEncryptionAlgorithmsLookupStrategy">
<constructor-arg>
<bean
- class="net.shibboleth.oidc.profile.config.navigate.ProviderMetadataStringValuesLookupFunction"
- c:keyName="request_object_encryption_enc_values_supported"/>
+ class="net.shibboleth.oidc.profile.config.navigate.ProviderMetadataStringValuesLookupFunction"
+ c:keyName="request_object_encryption_enc_values_supported" />
</constructor-arg>
</bean>
</property>
</bean>
-
+
<bean id="RequestObjectEncryptionConfigurationLookup" lazy-init="true" scope="prototype"
class="net.shibboleth.oidc.profile.config.navigate.JWTEncryptionConfigurationLookupFunction" />
-
- <bean id="BuildRequestObject" class="net.shibboleth.sp.oidc.profile.impl.BuildRequestObject"
- scope="prototype"
+
+ <bean id="BuildRequestObject" class="net.shibboleth.sp.oidc.profile.impl.BuildRequestObject" scope="prototype"
p:claimsSetIsValidPredicate="#{getObject('shibboleth.sp.oidc.RequestObjectClaimsSetIsValidPredicate')}"
- p:requestObjectToBeSignedPredicate-ref="SignRequestObjectCondition"
- p:customClaimsStrategy="#{getObject('shibboleth.sp.oidc.CustomRequestObjectClaimsStrategy')}"/>
-
+ p:requestObjectToBeSignedPredicate-ref="SignRequestObjectCondition"
+ p:customClaimsStrategy="#{getObject('shibboleth.sp.oidc.CustomRequestObjectClaimsStrategy')}" />
+
<bean id="SignRequestObjectCondition" scope="prototype"
class="net.shibboleth.oidc.profile.config.logic.SignRequestObjectPredicate"
p:relyingPartyContextLookupStrategy-ref="shibboleth.ChildLookup.RelyingParty" />
-
-
- <bean id="HandleOutboundMessage"
- class="net.shibboleth.idp.profile.impl.WebFlowMessageHandlerAdaptor" scope="prototype"
- c:messageHandler-ref="PreEncodeMessageHandler"
- c:executionDirection="OUTBOUND">
+
+ <!--
+ Store off required authentication state via the storage token manager. The token returned from the storage manager
+ is referenced in the correlation cookie for later recovery.
+ -->
+ <bean id="StoreAuthenticationState"
+ class="net.shibboleth.sp.oidc.profile.impl.MapStateValueToStateToken" scope="prototype"
+ p:createOutputObjects="true"
+ p:errorFatal="%{sp.stateToken.errorsFatal:true}"
+ p:stateValueLookupStrategy-ref="AuthenticationRequestStateForStorageStrategy"
+ p:stateTokenConsumer-ref="AuthenticationStateTokenConsumer" />
+
+ <!-- A consumer that stores off the authentication state token (key) -->
+ <bean id="AuthenticationStateTokenConsumer"
+ class="net.shibboleth.sp.oidc.profile.impl.SetAuthenticationStateTokenConsumer" />
+
+ <!-- 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="HandleOutboundMessage" class="net.shibboleth.idp.profile.impl.WebFlowMessageHandlerAdaptor"
+ scope="prototype" c:messageHandler-ref="PreEncodeMessageHandler" c:executionDirection="OUTBOUND">
<property name="errorEvent">
<util:constant static-field="org.opensaml.profile.action.EventIds.MESSAGE_PROC_ERROR" />
</property>
</bean>
-
- <!-- TODO Might not need all of these in the preencode step if we can add the state before...but signing and encrypting the RO still might work here -->
+
<bean id="PreEncodeMessageHandler" class="org.opensaml.messaging.handler.impl.BasicMessageHandlerChain"
scope="prototype">
<property name="handlers">
@@ -266,7 +302,9 @@
<bean id="AddState" class="net.shibboleth.oidc.profile.messaging.handler.impl.AddStateHandler"
scope="prototype">
<property name="parameterValueLookupStrategy">
- <bean class="net.shibboleth.sp.oidc.profile.impl.StateLookupStrategy" scope="prototype"/>
+ <bean class="net.shibboleth.sp.oidc.profile.impl.StateLookupStrategy" scope="prototype"
+ p:dataSealer-ref="#{'%{sp.dataSealer:shibboleth.DataSealer}'.trim()}"
+ p:sealStatePredicate="#{getObject('%{sp.oidc.sealOAuthStatePredicate:}') ?: ((%{sp.oidc.sealOAuthState:true}) ? getObject('shibboleth.Conditions.TRUE') : getObject('shibboleth.Conditions.FALSE'))}"/>
</property>
</bean>
@@ -274,84 +312,60 @@
class="net.shibboleth.oidc.profile.messaging.handler.impl.BuildPlainRequestObjectJWT"
scope="prototype" />
- <bean id="SignRequestObject" class="net.shibboleth.oidc.security.impl.SignJWTHandler"
+ <bean id="SignRequestObject" class="net.shibboleth.oidc.security.impl.SignJWTHandler"
scope="prototype" p:logName="RequestObject">
<property name="claimsToSignLookupStrategy">
<bean
class="net.shibboleth.sp.oidc.context.navigate.JWTClaimsSetFromRequestObjectLookupFunction" />
</property>
<property name="jwtUpdateConsumer">
- <bean
- class="net.shibboleth.sp.oidc.context.navigate.RequestObjectTokenUpdateStrategy" />
+ <bean class="net.shibboleth.sp.oidc.context.navigate.RequestObjectTokenUpdateStrategy" />
</property>
- </bean>
+ </bean>
- <bean id="EncryptRequestObject"
- class="net.shibboleth.oidc.security.impl.EncryptJWTHandler" scope="prototype"
- p:logName="RequestObject">
+ <bean id="EncryptRequestObject" class="net.shibboleth.oidc.security.impl.EncryptJWTHandler"
+ scope="prototype" p:logName="RequestObject">
<property name="payloadToEncryptLookupStrategy">
<bean
class="net.shibboleth.sp.oidc.context.navigate.PayloadFromRequestObjectLookupFunction" />
</property>
<property name="jwtUpdateConsumer">
- <bean
- class="net.shibboleth.sp.oidc.context.navigate.RequestObjectTokenUpdateStrategy" />
+ <bean class="net.shibboleth.sp.oidc.context.navigate.RequestObjectTokenUpdateStrategy" />
</property>
</bean>
- <bean id="SetAuthenticationRequestTime"
- class="net.shibboleth.oidc.profile.messaging.handler.impl.SetAuthenticationRequestTimeHandler" scope="prototype"/>
+ <bean id="SetAuthenticationRequestTime"
+ class="net.shibboleth.oidc.profile.messaging.handler.impl.SetAuthenticationRequestTimeHandler"
+ scope="prototype" />
</list>
</property>
</bean>
-
- <!-- TODO not having a state token to issue a correlation cookie has been set to fatal -->
- <bean id="IssueNonceCorrelationCookie" class="net.shibboleth.sp.oidc.profile.impl.IssueStateCookie" scope="prototype"
- p:cookieManager-ref="shibboleth.sp.RemotedCookieManager"
- p:cookiePrefix="#{'%{sp.correlation.cookiePrefix:__Host-shibsp_req_}'.trim()}"
- p:createOutputObjects="true"
- p:errorFatal="%{sp.stateToken.errorsFatal:true}"
- p:stateValueLookupStrategy-ref="NonceFromStateStrategy" />
-
- <bean id="NonceFromStateStrategy" parent="shibboleth.Functions.Compose">
- <constructor-arg name="g">
- <bean class="net.shibboleth.sp.oidc.messaging.navigate.NonceFromAuthenticationRequestStateLookupFunction" />
- </constructor-arg>
- <constructor-arg name="f">
- <bean parent="shibboleth.Functions.Compose" c:f-ref="shibboleth.MessageContextLookup.Outbound">
- <constructor-arg name="g">
- <bean parent="shibboleth.Functions.Expression"
- c:expression="#input.getMessage()"/>
- </constructor-arg>
- </bean>
- </constructor-arg>
- </bean>
-
- <!-- Issue a cookie with state information that is needed to be recovered to validate the response -->
- <bean id="IssueAuthnStateCookie" class="net.shibboleth.sp.oidc.profile.impl.IssueStateCookie" scope="prototype"
+
+ <!--
+ Issue the correlation cookie that relates request to response and stores of parameters needed to recover
+ authentication state when processing the response.
+ -->
+ <bean id="IssueRequestCorrelationCookie" class="net.shibboleth.sp.oidc.profile.impl.IssueStateCookie"
+ scope="prototype"
p:cookieManager-ref="shibboleth.sp.RemotedCookieManager"
- p:cookiePrefix="#{'%{sp.authnstate.cookiePrefix:__Host-shibsp_authnstate_}'.trim()}"
- p:createOutputObjects="true"
- p:errorFatal="%{sp.stateToken.errorsFatal:true}"
- p:stateValueLookupStrategy-ref="AuthenticationRequestStateForStorageStrategy" />
-
- <!-- TODO TRIAL (Ensure this is sealed)-->
- <bean id="MapAuthenticationStateToStateToken" class="net.shibboleth.sp.oidc.profile.impl.MapStateToStateToken" scope="prototype"
+ p:cookiePrefix="#{'%{sp.correlation.cookiePrefix:__Host-shibsp_req_}'.trim()}"
p:createOutputObjects="true"
p:errorFatal="%{sp.stateToken.errorsFatal:true}"
- p:stateValueLookupStrategy-ref="AuthenticationRequestStateForStorageStrategy"/>
-
- <!-- 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')}"/>
+ p:stateValueLookupStrategy-ref="RequestCorrelationCookieFromStateStrategy"/>
- <bean id="EncodeMessage" class="net.shibboleth.sp.profile.impl.EncodeMessage" scope="prototype"
- p:createOutputObjects="true"
- p:messageEncoderFactory-ref="messageEncoderFactory" />
+ <!--
+ Create a request correlation cookie. The cookie contains an authentication token that maps back to stored
+ authentication state alongside a request forgery protection nonce used to prevent CSRF attacks.
+ -->
+ <bean id="RequestCorrelationCookieFromStateStrategy"
+ class="net.shibboleth.sp.oidc.messaging.navigate.RequestCorrelationFromStateLookupFunction"
+ c:authenticationRequestDataStateLookupStrategy-ref="shibboleth.ChildLookup.AuthenticationRequestStateDataFromOutbound"
+ c:authenticationRequestLookupStrategy-ref="shibboleth.AuthenticationRequestLookup.FromOutbound"
+ p:dataSealer-ref="#{'%{sp.dataSealer:shibboleth.DataSealer}'.trim()}"
+ p:sealStatePredicate="#{getObject('%{sp.oidc.sealCorrelationCookiePredicate:}') ?: ((%{sp.oidc.sealCorrelationCookie:true}) ? getObject('shibboleth.Conditions.TRUE') : getObject('shibboleth.Conditions.FALSE'))}"/>
+ <bean id="EncodeMessage" class="net.shibboleth.sp.profile.impl.EncodeMessage" scope="prototype"
+ p:createOutputObjects="true" p:messageEncoderFactory-ref="messageEncoderFactory" />
+
<!-- Message Encoder factory is a prototype to allow reuse of the encoders -->
<bean id="messageEncoderFactory"
class="net.shibboleth.oidc.profile.impl.AuthenticationRequestMessageEncoderFactory" scope="prototype"
@@ -370,11 +384,12 @@
<bean id="HTTPPostAuthnRequestEncoder"
class="net.shibboleth.oidc.profile.encoding.impl.HTTPPostAuthnRequestEncoder" init-method="" scope="prototype"
- p:velocityEngine-ref="shibboleth.VelocityEngine" p:httpServletResponseSupplier-ref="shibboleth.RemotedHttpServletResponseSupplier"
- p:authorizationParamsAreValidPredicate="#{getObject('%{sp.oidc.AuthzParamsValidPredicate:}'.trim())}"
+ p:velocityEngine-ref="shibboleth.VelocityEngine"
+ p:httpServletResponseSupplier-ref="shibboleth.RemotedHttpServletResponseSupplier"
+ p:authorizationParamsAreValidPredicate="#{getObject('%{sp.oidc.AuthzParamsValidPredicate:}'.trim())}"
p:cSPDigester="#{%{idp.encoders.cspEnabled:true} ? getObject('shibboleth.CSPDigester') : null}"
- p:cSPNonceGenerator="#{%{idp.encoders.cspEnabled:true} ? getObject('shibboleth.CSPNonce') : null}"/>
-
-
+ p:cSPNonceGenerator="#{%{idp.encoders.cspEnabled:true} ? getObject('shibboleth.CSPNonce') : null}" />
+
+
</beans>
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 6629579..aeee613 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
@@ -45,11 +45,11 @@
<transition to="ReselectFlow" />
</action-state>
- <action-state id="BuildOutboundMessage">
- <evaluate expression="HandleOutboundMessage" />
- <!-- <evaluate expression="MapAuthenticationStateToStateToken"/> TRIAL -->
- <evaluate expression="IssueNonceCorrelationCookie" />
- <evaluate expression="IssueAuthnStateCookie" />
+ <action-state id="BuildOutboundMessage">
+ <!-- Must map authentication state to a state token first, so we can add the token to the state in HandleOutboundMessage -->
+ <evaluate expression="StoreAuthenticationState"/>
+ <evaluate expression="HandleOutboundMessage" />
+ <evaluate expression="IssueRequestCorrelationCookie" />
<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 6bcbd1f..f78af5c 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
@@ -24,6 +24,7 @@
class="net.shibboleth.sp.profile.impl.WebFlowMessageHandlerAdaptor" scope="prototype" abstract="true"
c:executionDirection="OUTBOUND" />
+ <!-- TODO we do not need the Context suffix on these beans -->
<!-- Global Functions -->
<bean id="shibboleth.ChildLookupOrCreate.SecurityParametersContext"
class="org.opensaml.messaging.context.navigate.ChildContextLookup"
@@ -34,6 +35,10 @@
class="org.opensaml.messaging.context.navigate.ChildContextLookup"
c:type="#{ T(net.shibboleth.oidc.metadata.context.OIDCProviderMetadataContext) }" />
+ <bean id="shibboleth.ChildLookup.OAuthStateContext"
+ class="org.opensaml.messaging.context.navigate.ChildContextLookup"
+ c:type="#{ T(net.shibboleth.sp.oidc.context.OAuthStateContext) }" />
+
<bean id="shibboleth.ChildLookup.OIDCPeerEntityContext"
class="org.opensaml.messaging.context.navigate.ChildContextLookup"
c:type="#{ T(net.shibboleth.oidc.profile.messaging.context.OIDCPeerEntityContext) }" />
@@ -46,6 +51,15 @@
class="org.opensaml.messaging.context.navigate.ChildContextLookup"
c:type="#{ T(net.shibboleth.oidc.authn.context.OAuth2ClientAuthenticationContext) }"
c:createContext="true"/>
+
+ <bean id="shibboleth.ChildLookupOrCreate.CorrelationCookieStateContext"
+ class="org.opensaml.messaging.context.navigate.ChildContextLookup"
+ c:type="#{ T(net.shibboleth.sp.oidc.context.CorrelationCookieStateContext) }"
+ c:createContext="true"/>
+
+ <bean id="shibboleth.ChildLookup.CorrelationCookieStateContext"
+ class="org.opensaml.messaging.context.navigate.ChildContextLookup"
+ c:type="#{ T(net.shibboleth.sp.oidc.context.CorrelationCookieStateContext) }"/>
<bean id="shibboleth.ChildLookup.OAuth2ClientAuthenticationContext"
class="org.opensaml.messaging.context.navigate.ChildContextLookup"
@@ -89,6 +103,33 @@
</constructor-arg>
</bean>
+ <bean id="shibboleth.ChildLookupOrCreate.CorrelationCookieStateContextFromInboundOAuthStateContext" parent="shibboleth.Functions.Compose">
+ <constructor-arg name="g">
+ <ref bean="shibboleth.ChildLookupOrCreate.CorrelationCookieStateContext" />
+ </constructor-arg>
+ <constructor-arg name="f">
+ <ref bean="shibboleth.ChildLookup.OAuthStateContextFromInbound" />
+ </constructor-arg>
+ </bean>
+
+ <bean id="shibboleth.ChildLookup.CorrelationCookieStateContextFromOAuthStateContext" parent="shibboleth.Functions.Compose">
+ <constructor-arg name="g">
+ <ref bean="shibboleth.ChildLookup.CorrelationCookieStateContext" />
+ </constructor-arg>
+ <constructor-arg name="f">
+ <ref bean="shibboleth.ChildLookup.OAuthStateContext" />
+ </constructor-arg>
+ </bean>
+
+ <bean id="shibboleth.ChildLookup.OAuthStateContextFromInbound" parent="shibboleth.Functions.Compose">
+ <constructor-arg name="g">
+ <ref bean="shibboleth.ChildLookup.OAuthStateContext" />
+ </constructor-arg>
+ <constructor-arg name="f">
+ <ref bean="shibboleth.MessageContextLookup.Inbound" />
+ </constructor-arg>
+ </bean>
+
<bean id="shibboleth.ChildLookup.OAuth2ClientContextFromInbound" parent="shibboleth.Functions.Compose">
<constructor-arg name="g">
<ref bean="shibboleth.ChildLookup.OAuth2ClientContext" />
@@ -187,6 +228,16 @@
</constructor-arg>
</bean>
+ <bean id="shibboleth.ChildLookup.AuthenticationRequestStateDataFromOutbound"
+ parent="shibboleth.Functions.Compose">
+ <constructor-arg name="g">
+ <ref bean="shibboleth.ChildLookup.AuthnRequestStateDataContext" />
+ </constructor-arg>
+ <constructor-arg name="f">
+ <ref bean="shibboleth.MessageContextLookup.Outbound" />
+ </constructor-arg>
+ </bean>
+
<!-- Common Actions -->
<bean id="ProviderMetadataLookup" parent="WebFlowInboundMessageHandlerAdaptor" scope="prototype">
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 89ab7d6..4595c27 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
@@ -36,7 +36,6 @@ import org.mockito.Mockito;
import org.opensaml.messaging.decoder.MessageDecodingException;
import org.opensaml.profile.action.EventIds;
import org.opensaml.profile.context.ProfileRequestContext;
-import org.opensaml.saml.common.binding.SAMLBindingSupport;
import org.springframework.context.ApplicationContext;
import org.springframework.core.io.ClassPathResource;
import org.springframework.test.context.ContextConfiguration;
@@ -81,7 +80,7 @@ import net.shibboleth.sp.profile.impl.IssueCorrelationCookie;
inheritInitializers = false
)
@WebAppConfiguration
- at SuppressWarnings({ "unchecked", "rawtypes", "null" })
+ at SuppressWarnings({ "unchecked", "null" })
public class OIDCSessionInitiatorFlowTest extends AbstractSPFlowTest {
/** Flow ID. */
@@ -561,15 +560,13 @@ public class OIDCSessionInitiatorFlowTest extends AbstractSPFlowTest {
authnRequest = decodeRedirect(redirectURL,
input != null ? input.getmember(SPConstants.STATE).string() : null);
} else {
- //TODO what to pull out if in the POST body
+ //TODO what to pull out if in the POST body. You can not send a POST message to the authz redirect, so is this needed?
final byte[] body = http.getmember("response.data").unsafe_string();
Assert.assertNotNull(body);
// Not trivial to consider parsing the form, so just bypass that step.
final Object oidc = prc.ensureOutboundMessageContext().ensureMessage();
assert oidc instanceof AuthenticationRequest;
authnRequest = (AuthenticationRequest) oidc;
- Assert.assertEquals(SAMLBindingSupport.getRelayState(prc.ensureOutboundMessageContext()),
- input != null ? input.getmember(SPConstants.STATE).string() : null);
}
assert authnRequest != null;
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 bef9712..8812cfb 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
@@ -174,14 +174,16 @@ public class OIDCTokenConsumerFlowTest extends AbstractSPFlowTest {
final AuthenticationSuccessResponse response =
buildOIDCAuthorizationCodeResponse(TestConstants.RESPONSE_URL, ResponseMode.QUERY,
- TestConstants.STATE_STRING);
+ TestConstants.buildOAuthStateString(TestConstants.STATE_TOKEN,
+ TestConstants.AUTHENTICATION_RFP));
final DDF input = buildRemotedQueryStringResponse(response);
// Add cookies
input.addmember("http.headers.Cookie").unsafe_string(TestConstants.buildCookieHeader(
- TestConstants.AUTHENTICATION_STATE,
+ TestConstants.STATE_TOKEN,
+ TestConstants.AUTHENTICATION_STATE_TOKEN,
TestConstants.APPLICATION_ID,
- TestConstants.AUTHENTICATION_NONCE,
+ TestConstants.AUTHENTICATION_RFP,
TestConstants.TARGET_URL,
TestConstants.buildAuthenticationState(null, false, null)));
@@ -210,19 +212,21 @@ public class OIDCTokenConsumerFlowTest extends AbstractSPFlowTest {
final AuthenticationSuccessResponse response =
buildOIDCAuthorizationCodeResponse(TestConstants.RESPONSE_URL, ResponseMode.QUERY,
- TestConstants.STATE_STRING);
+ TestConstants.buildOAuthStateString(TestConstants.STATE_TOKEN,
+ TestConstants.AUTHENTICATION_RFP));
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.STATE_TOKEN,
+ TestConstants.AUTHENTICATION_STATE_TOKEN,
+ TestConstants.APPLICATION_ID_PRIVATE_KEY_JWT,
+ TestConstants.AUTHENTICATION_RFP,
TestConstants.TARGET_URL,
TestConstants.buildAuthenticationState(null, false, null)));
// Use application which requires private_key_jwt
- setApplicationRequest("test-oidc-application-with-ro-private-key-jwt", input);
+ setApplicationRequest(TestConstants.APPLICATION_ID_PRIVATE_KEY_JWT, input);
final FlowExecutionResult result = flowExecutor.launchExecution(TestConstants.FLOW_ID, null, externalContext);
assertFlowExecutionResult(result, TestConstants.FLOW_ID);
@@ -235,7 +239,7 @@ public class OIDCTokenConsumerFlowTest extends AbstractSPFlowTest {
}
/**
- * Test successful flow with a signed id_token and a plain user info response.
+ * Test successful flow with a signed id_token and a plain user info response with max_age set.
*
* @throws IOException on error
*/
@@ -247,14 +251,16 @@ public class OIDCTokenConsumerFlowTest extends AbstractSPFlowTest {
final AuthenticationSuccessResponse response =
buildOIDCAuthorizationCodeResponse(TestConstants.RESPONSE_URL, ResponseMode.QUERY,
- TestConstants.STATE_STRING);
+ TestConstants.buildOAuthStateString(TestConstants.STATE_TOKEN,
+ TestConstants.AUTHENTICATION_RFP));
final DDF input = buildRemotedQueryStringResponse(response);
// Add cookies
input.addmember("http.headers.Cookie").unsafe_string(TestConstants.buildCookieHeader(
- TestConstants.AUTHENTICATION_STATE,
+ TestConstants.STATE_TOKEN,
+ TestConstants.AUTHENTICATION_STATE_TOKEN,
TestConstants.APPLICATION_ID,
- TestConstants.AUTHENTICATION_NONCE,
+ TestConstants.AUTHENTICATION_RFP,
TestConstants.TARGET_URL,
TestConstants.buildAuthenticationState(Duration.ofMinutes(1), true, null)));
@@ -282,14 +288,17 @@ public class OIDCTokenConsumerFlowTest extends AbstractSPFlowTest {
null), constructJSONUserInfoResponse());
final AuthenticationSuccessResponse response =
- buildOIDCAuthorizationCodeResponse(TestConstants.RESPONSE_URL, ResponseMode.QUERY, TestConstants.STATE_STRING);
+ buildOIDCAuthorizationCodeResponse(TestConstants.RESPONSE_URL, ResponseMode.QUERY,
+ TestConstants.buildOAuthStateString(TestConstants.STATE_TOKEN,
+ TestConstants.AUTHENTICATION_RFP));
final DDF input = buildRemotedQueryStringResponse(response);
// Add cookies
input.addmember("http.headers.Cookie").unsafe_string(TestConstants.buildCookieHeader(
- TestConstants.AUTHENTICATION_STATE,
+ TestConstants.STATE_TOKEN,
+ TestConstants.AUTHENTICATION_STATE_TOKEN,
TestConstants.APPLICATION_ID,
- TestConstants.AUTHENTICATION_NONCE,
+ TestConstants.AUTHENTICATION_RFP,
TestConstants.TARGET_URL,
TestConstants.buildAuthenticationState(Duration.ofMinutes(1), true, null)));
@@ -315,14 +324,17 @@ public class OIDCTokenConsumerFlowTest extends AbstractSPFlowTest {
Instant.now(), null, null), constructJSONUserInfoResponse());
final AuthenticationSuccessResponse response =
- buildOIDCAuthorizationCodeResponse(TestConstants.RESPONSE_URL, ResponseMode.QUERY, TestConstants.STATE_STRING);
+ buildOIDCAuthorizationCodeResponse(TestConstants.RESPONSE_URL, ResponseMode.QUERY,
+ TestConstants.buildOAuthStateString(TestConstants.STATE_TOKEN,
+ TestConstants.AUTHENTICATION_RFP));
final DDF input = buildRemotedQueryStringResponse(response);
// Add cookies
input.addmember("http.headers.Cookie").unsafe_string(TestConstants.buildCookieHeader(
- TestConstants.AUTHENTICATION_STATE,
+ TestConstants.STATE_TOKEN,
+ TestConstants.AUTHENTICATION_STATE_TOKEN,
TestConstants.APPLICATION_ID,
- TestConstants.AUTHENTICATION_NONCE,
+ TestConstants.AUTHENTICATION_RFP,
TestConstants.TARGET_URL,
TestConstants.buildAuthenticationState(null, false, null)));
@@ -350,14 +362,17 @@ public class OIDCTokenConsumerFlowTest extends AbstractSPFlowTest {
null, null), constructJWTUserInfoResponseSigned());
final AuthenticationSuccessResponse response =
- buildOIDCAuthorizationCodeResponse(TestConstants.RESPONSE_URL, ResponseMode.QUERY, TestConstants.STATE_STRING);
+ buildOIDCAuthorizationCodeResponse(TestConstants.RESPONSE_URL, ResponseMode.QUERY,
+ TestConstants.buildOAuthStateString(TestConstants.STATE_TOKEN,
+ TestConstants.AUTHENTICATION_RFP));
final DDF input = buildRemotedQueryStringResponse(response);
// Add cookies
input.addmember("http.headers.Cookie").unsafe_string(TestConstants.buildCookieHeader(
- TestConstants.AUTHENTICATION_STATE,
+ TestConstants.STATE_TOKEN,
+ TestConstants.AUTHENTICATION_STATE_TOKEN,
TestConstants.APPLICATION_ID,
- TestConstants.AUTHENTICATION_NONCE,
+ TestConstants.AUTHENTICATION_RFP,
TestConstants.TARGET_URL,
TestConstants.buildAuthenticationState(null, false, null)));
@@ -386,14 +401,17 @@ public class OIDCTokenConsumerFlowTest extends AbstractSPFlowTest {
null, Map.of("acr","loa2")), constructJWTUserInfoResponseSigned());
final AuthenticationSuccessResponse response =
- buildOIDCAuthorizationCodeResponse(TestConstants.RESPONSE_URL, ResponseMode.QUERY, TestConstants.STATE_STRING);
+ buildOIDCAuthorizationCodeResponse(TestConstants.RESPONSE_URL, ResponseMode.QUERY,
+ TestConstants.buildOAuthStateString(TestConstants.STATE_TOKEN,
+ TestConstants.AUTHENTICATION_RFP));
final DDF input = buildRemotedQueryStringResponse(response);
// Add cookies
input.addmember("http.headers.Cookie").unsafe_string(TestConstants.buildCookieHeader(
- TestConstants.AUTHENTICATION_STATE,
+ TestConstants.STATE_TOKEN,
+ TestConstants.AUTHENTICATION_STATE_TOKEN,
TestConstants.APPLICATION_ID,
- TestConstants.AUTHENTICATION_NONCE,
+ TestConstants.AUTHENTICATION_RFP,
TestConstants.TARGET_URL,
TestConstants.buildAuthenticationState(null, false, CollectionSupport.listOf("loa1"))));
@@ -424,14 +442,17 @@ public class OIDCTokenConsumerFlowTest extends AbstractSPFlowTest {
null, Map.of("acr","loa1")), constructJWTUserInfoResponseSigned());
final AuthenticationSuccessResponse response =
- buildOIDCAuthorizationCodeResponse(TestConstants.RESPONSE_URL, ResponseMode.QUERY, TestConstants.STATE_STRING);
+ buildOIDCAuthorizationCodeResponse(TestConstants.RESPONSE_URL, ResponseMode.QUERY,
+ TestConstants.buildOAuthStateString(TestConstants.STATE_TOKEN,
+ TestConstants.AUTHENTICATION_RFP));
final DDF input = buildRemotedQueryStringResponse(response);
// Add cookies
input.addmember("http.headers.Cookie").unsafe_string(TestConstants.buildCookieHeader(
- TestConstants.AUTHENTICATION_STATE,
+ TestConstants.STATE_TOKEN,
+ TestConstants.AUTHENTICATION_STATE_TOKEN,
TestConstants.APPLICATION_ID,
- TestConstants.AUTHENTICATION_NONCE,
+ TestConstants.AUTHENTICATION_RFP,
TestConstants.TARGET_URL,
TestConstants.buildAuthenticationState(null, false, CollectionSupport.listOf("loa1"))));
@@ -460,14 +481,17 @@ public class OIDCTokenConsumerFlowTest extends AbstractSPFlowTest {
null, Map.of("azp", TestConstants.CLIENT_ID)), constructJWTUserInfoResponseSigned());
final AuthenticationSuccessResponse response =
- buildOIDCAuthorizationCodeResponse(TestConstants.RESPONSE_URL, ResponseMode.QUERY, TestConstants.STATE_STRING);
+ buildOIDCAuthorizationCodeResponse(TestConstants.RESPONSE_URL, ResponseMode.QUERY,
+ TestConstants.buildOAuthStateString(TestConstants.STATE_TOKEN,
+ TestConstants.AUTHENTICATION_RFP));
final DDF input = buildRemotedQueryStringResponse(response);
// Add cookies
input.addmember("http.headers.Cookie").unsafe_string(TestConstants.buildCookieHeader(
- TestConstants.AUTHENTICATION_STATE,
+ TestConstants.STATE_TOKEN,
+ TestConstants.AUTHENTICATION_STATE_TOKEN,
TestConstants.APPLICATION_ID,
- TestConstants.AUTHENTICATION_NONCE,
+ TestConstants.AUTHENTICATION_RFP,
TestConstants.TARGET_URL,
TestConstants.buildAuthenticationState(null, false, null)));
@@ -494,14 +518,17 @@ public class OIDCTokenConsumerFlowTest extends AbstractSPFlowTest {
null, Map.of("azp", "bad-azp")), constructJWTUserInfoResponseSigned());
final AuthenticationSuccessResponse response =
- buildOIDCAuthorizationCodeResponse(TestConstants.RESPONSE_URL, ResponseMode.QUERY, TestConstants.STATE_STRING);
+ buildOIDCAuthorizationCodeResponse(TestConstants.RESPONSE_URL, ResponseMode.QUERY,
+ TestConstants.buildOAuthStateString(TestConstants.STATE_TOKEN,
+ TestConstants.AUTHENTICATION_RFP));
final DDF input = buildRemotedQueryStringResponse(response);
// Add cookies
input.addmember("http.headers.Cookie").unsafe_string(TestConstants.buildCookieHeader(
- TestConstants.AUTHENTICATION_STATE,
+ TestConstants.STATE_TOKEN,
+ TestConstants.AUTHENTICATION_STATE_TOKEN,
TestConstants.APPLICATION_ID,
- TestConstants.AUTHENTICATION_NONCE,
+ TestConstants.AUTHENTICATION_RFP,
TestConstants.TARGET_URL,
TestConstants.buildAuthenticationState(null, false, null)));
@@ -528,14 +555,17 @@ public class OIDCTokenConsumerFlowTest extends AbstractSPFlowTest {
Instant.now(), "bad-idtoken-nonce", null), constructJWTUserInfoResponseSigned());
final AuthenticationSuccessResponse response =
- buildOIDCAuthorizationCodeResponse(TestConstants.RESPONSE_URL, ResponseMode.QUERY, TestConstants.STATE_STRING);
+ buildOIDCAuthorizationCodeResponse(TestConstants.RESPONSE_URL, ResponseMode.QUERY,
+ TestConstants.buildOAuthStateString(TestConstants.STATE_TOKEN,
+ TestConstants.AUTHENTICATION_RFP));
final DDF input = buildRemotedQueryStringResponse(response);
// Add cookies
input.addmember("http.headers.Cookie").unsafe_string(TestConstants.buildCookieHeader(
- TestConstants.AUTHENTICATION_STATE,
+ TestConstants.STATE_TOKEN,
+ TestConstants.AUTHENTICATION_STATE_TOKEN,
TestConstants.APPLICATION_ID,
- TestConstants.AUTHENTICATION_NONCE,
+ TestConstants.AUTHENTICATION_RFP,
TestConstants.TARGET_URL,
TestConstants.buildAuthenticationState(null, false, null)));
@@ -550,6 +580,7 @@ public class OIDCTokenConsumerFlowTest extends AbstractSPFlowTest {
"JWT \"nonce\" claim has value bad-idtoken-nonce but should be bd1b5f211250c57e");
}
+ /** Test an error response from the Token endpoint.*/
@Test
public void testFail_ErrorFromTokenExchange() throws Exception {
@@ -557,27 +588,30 @@ public class OIDCTokenConsumerFlowTest extends AbstractSPFlowTest {
constructJWTUserInfoResponseSigned());
final AuthenticationSuccessResponse response =
- buildOIDCAuthorizationCodeResponse(TestConstants.RESPONSE_URL, ResponseMode.QUERY, TestConstants.STATE_STRING);
+ buildOIDCAuthorizationCodeResponse(TestConstants.RESPONSE_URL, ResponseMode.QUERY,
+ TestConstants.buildOAuthStateString(TestConstants.STATE_TOKEN,
+ TestConstants.AUTHENTICATION_RFP));
final DDF input = buildRemotedQueryStringResponse(response);
// Add cookies
input.addmember("http.headers.Cookie").unsafe_string(TestConstants.buildCookieHeader(
- TestConstants.AUTHENTICATION_STATE,
+ TestConstants.STATE_TOKEN,
+ TestConstants.AUTHENTICATION_STATE_TOKEN,
TestConstants.APPLICATION_ID,
- TestConstants.AUTHENTICATION_NONCE,
+ TestConstants.AUTHENTICATION_RFP,
TestConstants.TARGET_URL,
TestConstants.buildAuthenticationState(null, false, null)));
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);
+ final DDF output = assertOutputMessageEvent(result, EventIds.MESSAGE_PROC_ERROR);
+ System.out.println("test output: " + output.toString());
}
/**
- * Test a failure, nonce mismatch in the id_token.
+ * Test a success, using a nonce in the id_token
*
* @throws Exception on error
*/
@@ -588,14 +622,17 @@ public class OIDCTokenConsumerFlowTest extends AbstractSPFlowTest {
Instant.now(), TestConstants.ID_TOKEN_NONCE, null), constructJWTUserInfoResponseSigned());
final AuthenticationSuccessResponse response =
- buildOIDCAuthorizationCodeResponse(TestConstants.RESPONSE_URL, ResponseMode.QUERY, TestConstants.STATE_STRING);
+ buildOIDCAuthorizationCodeResponse(TestConstants.RESPONSE_URL, ResponseMode.QUERY,
+ TestConstants.buildOAuthStateString(TestConstants.STATE_TOKEN,
+ TestConstants.AUTHENTICATION_RFP));
final DDF input = buildRemotedQueryStringResponse(response);
// Add cookies
input.addmember("http.headers.Cookie").unsafe_string(TestConstants.buildCookieHeader(
- TestConstants.AUTHENTICATION_STATE,
+ TestConstants.STATE_TOKEN,
+ TestConstants.AUTHENTICATION_STATE_TOKEN,
TestConstants.APPLICATION_ID,
- TestConstants.AUTHENTICATION_NONCE,
+ TestConstants.AUTHENTICATION_RFP,
TestConstants.TARGET_URL,
TestConstants.buildAuthenticationState(null, false, null)));
@@ -612,21 +649,25 @@ public class OIDCTokenConsumerFlowTest extends AbstractSPFlowTest {
}
/**
- * Test failure, a bad nonce in the correlation cookie.
+ * Test failure, the RFP value in the returned state does not match that stored in the
+ * correlation cookie.
*
* @throws IOException on error
*/
@Test
- public void testFail_BadCorrelationNonceState_InCookie() throws Exception {
+ public void testFail_RequestForgeryProtectionValueMisMatch() throws Exception {
final AuthenticationSuccessResponse response =
- buildOIDCAuthorizationCodeResponse(TestConstants.RESPONSE_URL, ResponseMode.QUERY, TestConstants.STATE_STRING);
+ buildOIDCAuthorizationCodeResponse(TestConstants.RESPONSE_URL, ResponseMode.QUERY,
+ TestConstants.buildOAuthStateString(TestConstants.STATE_TOKEN,
+ TestConstants.AUTHENTICATION_RFP));
final DDF input = buildRemotedQueryStringResponse(response);
// Add cookies
input.addmember("http.headers.Cookie").unsafe_string(TestConstants.buildCookieHeader(
- TestConstants.AUTHENTICATION_STATE,
+ TestConstants.STATE_TOKEN,
+ TestConstants.AUTHENTICATION_STATE_TOKEN,
TestConstants.APPLICATION_ID,
- "bad-nonce", //this is a bad nonce
+ "bad-rfp", //this is a bad rfp value in the cookie
TestConstants.TARGET_URL,
TestConstants.buildAuthenticationState(null, false, null)));
@@ -635,27 +676,30 @@ public class OIDCTokenConsumerFlowTest extends AbstractSPFlowTest {
final FlowExecutionResult result = flowExecutor.launchExecution(TestConstants.FLOW_ID, null, externalContext);
assertFlowExecutionResult(result, TestConstants.FLOW_ID);
assertFlowExecutionOutcome(result.getOutcome());
- assertOutputMessageEvent(result, EventIds.MESSAGE_PROC_ERROR);
+ final DDF output = assertOutputMessageEvent(result, EventIds.MESSAGE_PROC_ERROR);
+ System.out.println("test output: " + output.toString());
}
/**
- * Test failure, a bad nonce in the authentication response. That is, what if somehow the return URL was manipulated
- * accidently or on purpose.
+ * Test failure: the correlation cookie state token in the OAuth state parameter does not reference the
+ * correlation cookie. Hence the correlation cookie can not be recovered, which is a fatal error.
*
* @throws IOException on error
*/
@Test
- public void testFail_BadCorrelationNonceState_InAuthnResponse() throws Exception {
+ public void testFail_CorrelationCookieMismatch() throws Exception {
final AuthenticationSuccessResponse response =
buildOIDCAuthorizationCodeResponse(TestConstants.RESPONSE_URL, ResponseMode.QUERY,
- TestConstants.STATE_STRING_DIFFERENT_STATE);
+ TestConstants.buildOAuthStateString(TestConstants.AUTHENTICATION_STATE_WRONG,
+ TestConstants.AUTHENTICATION_RFP));
final DDF input = buildRemotedQueryStringResponse(response);
// Add cookies
input.addmember("http.headers.Cookie").unsafe_string(TestConstants.buildCookieHeader(
- TestConstants.AUTHENTICATION_STATE,
+ TestConstants.STATE_TOKEN,
+ TestConstants.AUTHENTICATION_STATE_TOKEN,
TestConstants.APPLICATION_ID,
- TestConstants.AUTHENTICATION_NONCE,
+ TestConstants.AUTHENTICATION_RFP,
TestConstants.TARGET_URL,
TestConstants.buildAuthenticationState(null, false, null)));
@@ -675,13 +719,16 @@ public class OIDCTokenConsumerFlowTest extends AbstractSPFlowTest {
*/
@Test
public void testFail_ErrorResponse() throws Exception {
- final DDF input = buildRemotedQueryStringResponse(buildErrorResponse(TestConstants.STATE_STRING));
+ final DDF input = buildRemotedQueryStringResponse(buildErrorResponse(
+ TestConstants.buildOAuthStateString(TestConstants.STATE_TOKEN,
+ TestConstants.AUTHENTICATION_RFP)));
// Add cookies
input.addmember("http.headers.Cookie").unsafe_string(TestConstants.buildCookieHeader(
- TestConstants.AUTHENTICATION_STATE,
+ TestConstants.STATE_TOKEN,
+ TestConstants.AUTHENTICATION_STATE_TOKEN,
TestConstants.APPLICATION_ID,
- TestConstants.AUTHENTICATION_NONCE,
+ TestConstants.AUTHENTICATION_RFP,
TestConstants.TARGET_URL,
TestConstants.buildAuthenticationState(null, false, null)));
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 b581746..5408668 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
@@ -18,7 +18,6 @@ import static org.testng.Assert.fail;
import java.net.URI;
import java.net.URISyntaxException;
-import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.time.Instant;
@@ -35,10 +34,15 @@ 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.jose.util.StandardCharset;
+import net.minidev.json.JSONObject;
+import net.minidev.json.JSONValue;
import net.shibboleth.shared.annotation.constraint.NotEmpty;
import net.shibboleth.shared.codec.Base64Support;
+import net.shibboleth.shared.codec.EncodingException;
import net.shibboleth.sp.oidc.profile.AuthenticationRequestStateData;
+import net.shibboleth.sp.oidc.profile.OIDCConstants;
/** A class to hold constants used for tests.*/
public final class TestConstants {
@@ -63,80 +67,122 @@ 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 request forgery protection nonce sent in the authentication request and part of the state returned in an
+ * authentication response. Not the nonce in an id_token.
+ */
+ @Nonnull public static final String AUTHENTICATION_RFP = "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";
+ /** State token string used in cookie names. */
+ public static final String STATE_TOKEN = "1761316967710_1622a5c726da8f7b36e24f19eed82aea";
+
+ /** Authentication state token used to reference the stored authentication state. */
+ public static final String AUTHENTICATION_STATE_TOKEN = "1761316967710_authnstate";
+
+ /** State string used in cookie names. A state which should not match to the a correlation cookie. */
+ public static final String AUTHENTICATION_STATE_WRONG = "1761316967710_wrong";
/** Application ID used in cookie names. */
public static final String APPLICATION_ID = "test-oidc-application-with-ro";
+
+ /** Application ID used when private_key_jwt has been configured.*/
+ public static final String APPLICATION_ID_PRIVATE_KEY_JWT = "test-oidc-application-with-ro-private-key-jwt";
/** 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:
- *
- * <pre>
- * {"state":"1761316967710_1622a5c726da8f7b36e24f19eed82aea",
- * "nonce":"f26d922221226cd183729bf13247dbe2"}
- * </pre>
- * */
- public static final String STATE_STRING = """
- eyJzdGF0ZSI6IjE3NjEzMTY5Njc3MTBfMTYyMmE1YzcyNmRhOGY3YjM2ZTI0ZjE5ZWVkODJhZWEiLCJub25jZSI6ImYyNmQ5MjIyMjEyMjZjZDE4MzcyOWJmMTMyNDdkYmUyIn0
- """;
-
- /**
- * A returned state String consisting of a state and a nonce. The state has been changed, or is not expected:
- *
- * <pre>
- * {"state":"1761316967710_wrong",
- * "nonce":"f26d922221226cd183729bf13247dbe2"}
- * </pre>
- * */
- public static final String STATE_STRING_DIFFERENT_STATE = """
- eyJzdGF0ZSI6IjE3NjEzMTY5Njc3MTBfd3JvbmciLCJub25jZSI6ImYyNmQ5MjIyMjEyMjZjZDE4MzcyOWJmMTMyNDdkYmUyIn0=
- """;
/**
* 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_authnstate_{state}={authn_request_json};
+ * __Host-_shibsp_req_{stateToken}={(authnStateToken, rfp)}; (correlation cookie)
+ * __Host-shibsp_state_{appId}_{stateToken}={targetURL}; (resource URL state)
+ * __Host-shibsp_state_{appId}_{authnStateToken}={authn_request_json}; (authentication state)
* </pre>
*
- * @param state the state used for mapping response to the stored headers
+ * @param stateToken the token value used for mapping response to the stored correlation cookie and target URL
+ * @param authnStateToken the token value, held inside the 'req' correlation cookie, of the persisted authentication state.
* @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 rfp the request forgery protection nonce used to protect against CSRF in the request and response OAuth
+ * state parameter not to be confused with the OIDC ID token nonce. Stored inside the correlation cookie
* @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 byte[] buildCookieHeader(final String state, final String appID, final String nonce,
+ public static byte[] buildCookieHeader(final String stateToken,
+ final String authnStateToken, final String appID, final String rfp,
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);
+ final String authnJsonEnc = Base64Support.encodeURLSafe(authnRequestStateJSON.getBytes(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)
+ sb.append("__Host-shibsp_req_").append(stateToken).append('=')
+ .append(buildCorrelationCookieString(authnStateToken, rfp)).append(";\n");
+ sb.append("__Host-shibsp_state__").append(appID).append('_').append(stateToken)
.append('=').append(targetUrlB64).append("; \n");
- sb.append("__Host-shibsp_authnstate_").append(state).append('=').append(authnJsonEnc).append(";\n");
+ sb.append("__Host-shibsp_state__").append(appID).append('_').append(authnStateToken).append('=').append(authnJsonEnc).append(";\n");
System.out.println(sb.toString());
return sb.toString().getBytes("UTF-8");
}
+ /**
+ * Build the OAuth state string for the given state and RFP. This is used for the state parameter in the
+ * authentication request, and contains important information about the authentication request.
+ *
+ * @param state the state used to map to the correlation cookie bound to the browser session.
+ * @param rfp the request forgery protection nonce used to protect against CSRF in the request and response OAuth
+ * state parameter.
+ * @return the JSON serialized and Base64 encoded state string for the given parameters
+ */
+ public static String buildOAuthStateString(final String state, final String rfp) {
+ final JSONObject stateObject = new JSONObject();
+ stateObject.appendField(OIDCConstants.STATE_FIELD, state);
+ stateObject.appendField(OIDCConstants.RFP_FIELD,rfp);
+ final String stateJsonString = stateObject.toJSONString(JSONValue.COMPRESSION);
+ try {
+ final byte[] stateJsonAsBytes = stateJsonString.getBytes(StandardCharset.UTF_8);
+ assert stateJsonAsBytes != null;
+ return Base64Support.encodeURLSafe(stateJsonAsBytes);
+ } catch (final EncodingException e) {
+ fail(e.getMessage());
+ return null;
+ }
+
+ }
+
+ /**
+ * Build the correlation cookie value for the given authentication state token and RFP. The authnStateToken is used
+ * to recover the authentication state stored off in the state token manager. The RFP is used to protect against
+ * CSRF in the authentication request and response, and should be matched to that present in the OAuth state
+ * parameter. Both are stored in the cookie value, which is JSON serialized and Base64 encoded.
+ *
+ * @param authnStateToken the token value used for recovery of the authentication state stored off in the state
+ * token manager.
+ * @param rfp the request forgery protection nonce used to protect against CSRF in the request and response
+ * @return
+ */
+ public static String buildCorrelationCookieString(final String authnStateToken, final String rfp) {
+ final JSONObject stateObject = new JSONObject();
+ stateObject.appendField(OIDCConstants.AUTHN_STATE_FIELD, authnStateToken);
+ stateObject.appendField(OIDCConstants.RFP_FIELD,rfp);
+ final String stateJsonString = stateObject.toJSONString(JSONValue.COMPRESSION);
+ try {
+ final byte[] stateJsonAsBytes = stateJsonString.getBytes(StandardCharset.UTF_8);
+ assert stateJsonAsBytes != null;
+ return Base64Support.encodeURLSafe(stateJsonAsBytes);
+ } catch (final EncodingException e) {
+ fail(e.getMessage());
+ return null;
+ }
+ }
+
/**
* Build the authentication request state JSON for the given parameters. This is used for the state value, and
* contains important information about the authentication request.
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 a51e1a2..2964fcd 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
@@ -51,6 +51,10 @@ public class TestSPOIDCEnvironmentApplicationContextInitializer extends TestSPEn
mock.setProperty("sp.oidc.idtoken.validateAcrValue", "true");
// Turn off the sealing of authentication request state for tests
mock.setProperty("sp.oidc.sealAuthenticationState", "false");
+ // Turn off the sealing of the correlation cookie for tests
+ mock.setProperty("sp.oidc.sealCorrelationCookie", "false");
+ // Turn off the sealing of OAuth state for tests
+ mock.setProperty("sp.oidc.sealOAuthState", "false");
// Create a basic default client secret
mock.setProperty("sp.oidc.defaultClientSecret", "secret");
mock.setProperty("idp.additionalProperties",
diff --git a/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/decoding/impl/DecodeStateAsJsonObjectConsumer.java b/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/decoding/impl/DecodeStateAsJsonObjectConsumer.java
index 29c61b9..dcfa3b4 100644
--- a/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/decoding/impl/DecodeStateAsJsonObjectConsumer.java
+++ b/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/decoding/impl/DecodeStateAsJsonObjectConsumer.java
@@ -15,12 +15,16 @@
package net.shibboleth.sp.oidc.profile.decoding.impl;
import java.io.IOException;
+import java.nio.charset.StandardCharsets;
import java.util.function.Function;
+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.opensaml.profile.context.navigate.ParentProfileRequestContextLookup;
import org.slf4j.Logger;
import com.nimbusds.oauth2.sdk.id.State;
@@ -31,20 +35,80 @@ import net.minidev.json.JSONValue;
import net.minidev.json.parser.ParseException;
import net.shibboleth.shared.codec.Base64Support;
import net.shibboleth.shared.codec.DecodingException;
+import net.shibboleth.shared.component.AbstractIdentifiableInitializableComponent;
+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.context.OAuthStateContext;
/**
* Decodes the OAuth 2.0 state parameter from the {@link AuthenticationResponse} message
- * into a {@link JSONObject}.
+ * into a {@link JSONObject}. The state parameter is assumed to be base64 encoded.
+ *
+ * <p>If sealing is enabled, unwrap the state using the {@link DataSealer} provided. </p>
*
* <p>Returns true if the operation was successful. Returns false if the state cannot be decoded or parsed into a
* valid JSON object.</p>
*/
-public class DecodeStateAsJsonObjectConsumer implements Function<MessageContext, Boolean> {
+public class DecodeStateAsJsonObjectConsumer extends AbstractIdentifiableInitializableComponent
+ implements Function<MessageContext, Boolean> {
+
+ /** Lookup function for parent ProfileRequestContext. */
+ @Nonnull private static final ParentProfileRequestContextLookup<MessageContext> PRC_LOOKUP
+ = new ParentProfileRequestContextLookup<>();
/** Class logger. */
@Nonnull private final Logger log = LoggerFactory.getLogger(DecodeStateAsJsonObjectConsumer.class);
+
+
+ /** Optional data sealer to use. */
+ @Nullable private DataSealer dataSealer;
+
+ /**
+ * If the dataSealer is provided should it be used to unwrap the state? Defaults to true,
+ * that is, if the dataSealer is provided, always attempt to unwrap the state.
+ */
+ @Nonnull private Predicate<ProfileRequestContext> unsealState;
+
+ /** Constructor.*/
+ public DecodeStateAsJsonObjectConsumer() {
+ unsealState = PredicateSupport.alwaysTrue();
+ }
+
+ /**
+ * 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
@@ -63,15 +127,40 @@ public class DecodeStateAsJsonObjectConsumer implements Function<MessageContext,
try {
final String stateValue = responseState.getValue();
- assert stateValue != null;
+ assert stateValue != null;
+ Object stateObjectParsed = null;
+
+ // If sealed or not sealed, we base64 URL decode the state value first. This therefore assumes the state
+ // value is always base64 encoded. In the case of sealed state, the sealed value is also base64 (non-url)
+ // encoded, so we base64 URL decode to return back to the base64 encoded value to unwrap.
final byte[] decodedBytes = Base64Support.decodeURLSafe(stateValue);
- final JSONObject jsonState = (JSONObject) JSONValue.parseWithException(decodedBytes);
- if (jsonState != null) {
- messageContext.ensureSubcontext(OAuthStateContext.class)
- .setState(responseState.getValue())
- .setStateJson(jsonState);
+
+ final var localDataSealer = dataSealer;
+ if (localDataSealer != null && unsealState.test(PRC_LOOKUP.apply(messageContext))) {
+ final String decodedString = new String(decodedBytes, StandardCharsets.UTF_8);
+ log.trace("Attempting to unseal state value '{}'", decodedString);
+ final String unwrapped = localDataSealer.unwrap(decodedString);
+ log.trace("Unwrapped state '{}'", unwrapped);
+ stateObjectParsed = JSONValue.parseWithException(unwrapped);
+ } else {
+ final String decodedString = new String(decodedBytes, StandardCharsets.UTF_8);
+ stateObjectParsed = JSONValue.parseWithException(decodedString);
+
+ }
+
+
+ if (!(stateObjectParsed instanceof JSONObject)) {
+ log.debug("OAuth state did not decode to a JSON object");
+ return false;
}
- } catch (DecodingException | IOException | ParseException e) {
+ final JSONObject jsonState = (JSONObject) stateObjectParsed;
+ log.trace("Decoded JSON state '{}'", stateObjectParsed);
+
+ messageContext.ensureSubcontext(OAuthStateContext.class)
+ .setState(responseState.getValue())
+ .setStateJson(jsonState);
+
+ } catch (DecodingException | ParseException | DataSealerException e) {
log.debug("Unable to decode OAuth state into a JSON token", e);
return false;
}
diff --git a/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/IssueStateCookie.java b/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/IssueStateCookie.java
index a516fb7..309848b 100644
--- a/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/IssueStateCookie.java
+++ b/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/IssueStateCookie.java
@@ -148,7 +148,12 @@ public class IssueStateCookie extends AbstractApplicationAction {
stateValue = stateValueLookupStrategy.apply(profileRequestContext);
if (stateValue == null) {
- log.debug("{} No cookie contents available, skipping creation of state cookie", getLogPrefix());
+ if (errorFatal) {
+ log.warn("{} State value was missing", getLogPrefix());
+ ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_MESSAGE);
+ } else {
+ log.debug("{} No cookie contents available, skipping creation of state cookie", getLogPrefix());
+ }
return false;
}
diff --git a/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/MapStateTokenToStateValue.java b/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/MapStateTokenToStateValue.java
new file mode 100644
index 0000000..2e09a18
--- /dev/null
+++ b/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/MapStateTokenToStateValue.java
@@ -0,0 +1,167 @@
+/*
+ * 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.io.IOException;
+import java.util.function.BiFunction;
+import java.util.function.Function;
+
+import javax.annotation.Nonnull;
+
+import org.opensaml.profile.action.ActionSupport;
+import org.opensaml.profile.action.EventIds;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.slf4j.Logger;
+
+import net.shibboleth.shared.annotation.constraint.NonnullAfterInit;
+import net.shibboleth.shared.component.ComponentInitializationException;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.primitive.LoggerFactory;
+import net.shibboleth.sp.context.AgentRequestContext;
+import net.shibboleth.sp.messaging.RemotedHttpServletRequestResponseContext;
+import net.shibboleth.sp.profile.AbstractApplicationAction;
+
+
+/**
+ * Profile action that recovers a state value from a state token and uses a customisable strategy to set it onto the
+ * {@link ProfileRequestContext}.
+ *
+ * <p>Failures encountered during processing may be treated as fatal according to the {@link #errorFatal} flag. If
+ * fatal and an error occurs, an {@link EventIds#INVALID_MESSAGE} event is raised.</p>
+ *
+ * <p>This action does not interpret the state value itself. Instead, the token value consumer is expected to
+ * understand the structure and semantics of the recovered state value and set it on the {@link ProfileRequestContext}
+ * appropriately.</p>
+ */
+public class MapStateTokenToStateValue extends AbstractApplicationAction {
+
+ /** Class logger. */
+ @Nonnull private final Logger log = LoggerFactory.getLogger(MapStateTokenToStateValue.class);
+
+ /** Lookup strategy for state token. */
+ @NonnullAfterInit private Function<ProfileRequestContext,String> stateTokenLookupStrategy;
+
+ /**
+ * A consumer {@link BiFunction} that process the state value and adds it to the profile request context. The
+ * returned boolean indicates if the consumer function was successful or not. An unsuccessful outcome may trigger
+ * an error depending on the {@link #errorFatal} flag.
+ */
+ @NonnullAfterInit private BiFunction<ProfileRequestContext, byte[], Boolean> stateValueConsumer;
+
+ /** Whether an error mapping the state token to a state value is fatal. */
+ private boolean errorFatal;
+
+ /**
+ * Sets the lookup strategy to obtain the protocol specific state token.
+ *
+ * @param strategy lookup strategy
+ */
+ public void setStateTokenLookupStrategy(@Nonnull final Function<ProfileRequestContext,String> strategy) {
+ checkSetterPreconditions();
+ stateTokenLookupStrategy = Constraint.isNotNull(strategy, "State token lookup strategy cannot be null");
+ }
+
+ /**
+ * Set the state value consumer to add the recovered state value to the profile request context. The return value
+ * indicates if the consumer function was successful or not. An unsuccessful outcome may trigger an error
+ * depending on the {@link #errorFatal} flag.
+ *
+ * @param consumer The consumer to set.
+ */
+ public void setStateValueConsumer(final BiFunction<ProfileRequestContext, byte[], Boolean> consumer) {
+ checkSetterPreconditions();
+ stateValueConsumer = Constraint.isNotNull(consumer, "StateValueConsumer can not be null");
+ }
+
+ /**
+ * Sets whether an error looking up a state value from 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;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected void doInitialize() throws ComponentInitializationException {
+ super.doInitialize();
+
+ if (stateTokenLookupStrategy == null) {
+ throw new ComponentInitializationException("State token lookup strategy cannot be null");
+ }
+ if (stateValueConsumer == null) {
+ throw new ComponentInitializationException("State value consumer cannot be null");
+ }
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
+
+ ensureOutputObjects();
+
+ final AgentRequestContext agentRequestContext = ensureAgentRequestContext();
+
+ // We do the crazy stuff to accomodate cookie-backed state management
+ // (and to get the relevant state token in the first place).
+ try {
+ RemotedHttpServletRequestResponseContext.loadCurrent(agentRequestContext.getRemotedHttpServletRequest(),
+ agentRequestContext.getRemotedHttpServletResponse());
+
+ final String token = stateTokenLookupStrategy.apply(profileRequestContext);
+ if (token == null) {
+ log.debug("{} No state token returned from lookup strategy", getLogPrefix());
+ if (errorFatal) {
+ ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_MESSAGE);
+ }
+ return;
+ }
+
+ final byte[] stateValue = ensureApplication().getStateTokenManager().recoverFromStateToken(
+ ensureAgent(), ensureApplication(), token);
+
+ if (stateValue == null && !errorFatal) {
+ log.debug("{} No state value found for state token '{}'", getLogPrefix(), token);
+ return;
+ } else if (stateValue == null && errorFatal){
+ log.debug("{} No state value found for state token '{}'", getLogPrefix(), token);
+ ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_MESSAGE);
+ return;
+ }
+
+ final Boolean success = stateValueConsumer.apply(profileRequestContext, stateValue);
+ // Treat null as failure
+ if ((success == null || Boolean.FALSE.equals(success)) && errorFatal) {
+ log.warn("{} Could not set state value", getLogPrefix());
+ ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_MESSAGE);
+ return;
+ }
+
+ } catch (final IOException e) {
+ log.warn("{} Exception recovering state value from state token", getLogPrefix(), e);
+ if (errorFatal) {
+ ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_MESSAGE);
+ }
+ } finally {
+ RemotedHttpServletRequestResponseContext.clearCurrent();
+ }
+ }
+
+}
diff --git a/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/MapStateToStateToken.java b/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/MapStateValueToStateToken.java
similarity index 64%
rename from sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/MapStateToStateToken.java
rename to sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/MapStateValueToStateToken.java
index b0a13ab..167d50e 100644
--- a/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/MapStateToStateToken.java
+++ b/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/MapStateValueToStateToken.java
@@ -16,6 +16,7 @@ package net.shibboleth.sp.oidc.profile.impl;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
+import java.util.function.BiConsumer;
import java.util.function.Function;
import javax.annotation.Nonnull;
@@ -26,35 +27,53 @@ import org.opensaml.profile.context.ProfileRequestContext;
import org.slf4j.Logger;
import net.shibboleth.shared.annotation.constraint.NonnullAfterInit;
-import net.shibboleth.shared.annotation.constraint.NonnullBeforeExec;
+import net.shibboleth.shared.component.ComponentInitializationException;
import net.shibboleth.shared.logic.Constraint;
import net.shibboleth.shared.primitive.LoggerFactory;
import net.shibboleth.sp.context.AgentRequestContext;
-import net.shibboleth.sp.ddf.DDF;
import net.shibboleth.sp.messaging.RemotedHttpServletRequestResponseContext;
import net.shibboleth.sp.profile.AbstractApplicationAction;
-import net.shibboleth.sp.profile.SPConstants;
+import net.shibboleth.sp.profile.StateTokenManager;
/**
- *
+ *
+ * An action that extracts a state value from the {@link ProfileRequestContext} using a configured lookup strategy
+ * and maps that value into an application-managed state token. The resulting token is then stored via a configured
+ * consumer for later use: such as placement into an OAuth 2.0 "state" parameter or other correlation mechanism.
+ *
+ * <p>The action delegates the actual token creation and storage to the application's
+ * {@link StateTokenManager}.</p>
+ *
+ *
+ * @event {@link EventIds#IO_ERROR}
*/
-public class MapStateToStateToken extends AbstractApplicationAction {
+public class MapStateValueToStateToken extends AbstractApplicationAction {
/** Class logger. */
- @Nonnull private final Logger log = LoggerFactory.getLogger(MapStateToStateToken.class);
+ @Nonnull private final Logger log = LoggerFactory.getLogger(MapStateValueToStateToken.class);
/** Whether an error constructing a state token is fatal. */
private boolean errorFatal;
- /** Agent input. */
- @NonnullBeforeExec private DDF input;
-
- /** Target resource to operate on. */
- @NonnullBeforeExec private byte[] target;
-
- /** Lookup strategy for the contents of the state cookie. */
+ /** Lookup strategy for the contents of the state token. */
@NonnullAfterInit private Function<ProfileRequestContext,String> stateValueLookupStrategy;
+ /** A consumer that adds the state token to the profile request context.*/
+ @NonnullAfterInit private BiConsumer<ProfileRequestContext, String> stateTokenConsumer;
+
+ /** {@inheritDoc} */
+ @Override
+ protected void doInitialize() throws ComponentInitializationException {
+ super.doInitialize();
+
+ if (stateValueLookupStrategy == null) {
+ throw new ComponentInitializationException("StateValueLookupStrategy cannot be null");
+ }
+ if (stateTokenConsumer == null) {
+ throw new ComponentInitializationException("StateTokenConsumer cannot be null");
+ }
+ }
+
/**
* Sets whether an error computing a state token should result in a fatal event.
@@ -70,7 +89,7 @@ public class MapStateToStateToken extends AbstractApplicationAction {
}
/**
- * Sets the lookup strategy for obtaining the contents of the state cookie.
+ * Sets the lookup strategy for obtaining the contents of the state token.
*
* @param strategy lookup strategy
*/
@@ -80,34 +99,15 @@ public class MapStateToStateToken extends AbstractApplicationAction {
stateValueLookupStrategy = Constraint.isNotNull(strategy, "State value lookup strategy cannot be null");
}
- /** {@inheritDoc} */
- @Override
- protected boolean doPreExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
- if (!super.doPreExecute(profileRequestContext)) {
- return false;
- }
-
- input = ensureAgentRequestContext().getInput();
- if (input == null) {
- log.debug("{} Input message was absent", getLogPrefix());
- return false;
- }
-
-// if (input.getmember(SPConstants.STATE).isstring()) {
-// log.debug("{} Input message already contains {} parameter", getLogPrefix(), SPConstants.STATE);
-// return false;
-// }
-//
-// target = input.getmember(SPConstants.TARGET).unsafe_string();
-// if (target == null) {
-// log.warn("{} Input message did not contain {} member", getLogPrefix(), SPConstants.TARGET);
-// ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_MESSAGE);
-// return false;
-// }
-//
-// ensureAgentRequestContext().setTargetURL(target);
+ /**
+ * Set the consumer used to store off the state token onto the profile request context.
+ *
+ * @param consumer The stateTokenConsumer to set.
+ */
+ public void setStateTokenConsumer(@Nonnull final BiConsumer<ProfileRequestContext, String> consumer) {
+ checkSetterPreconditions();
- return true;
+ stateTokenConsumer = Constraint.isNotNull(consumer, "StateTokenConsumer can not be null");
}
/** {@inheritDoc} */
@@ -117,12 +117,6 @@ public class MapStateToStateToken extends AbstractApplicationAction {
ensureOutputObjects();
final AgentRequestContext agentRequestContext = ensureAgentRequestContext();
-
- String state = null;
- if (input.getmember(SPConstants.STATE).isstring()) {
- log.debug("{} Input message already contains {} parameter", getLogPrefix(), SPConstants.STATE);
- state = input.getmember(SPConstants.STATE).string();
- }
// We do the crazy stuff to accomodate cookie-backed state management.
try {
@@ -134,16 +128,18 @@ public class MapStateToStateToken extends AbstractApplicationAction {
if (stateValue != null) {
//TODO UTF-8?
final byte[] stateValueBytes = stateValue.getBytes(StandardCharsets.UTF_8);
+ // b64 encode is handled by the state manager
assert stateValueBytes != null;
final String token = ensureApplication().getStateTokenManager().preserveToStateToken(
ensureAgent(), ensureApplication(), stateValueBytes);
- if (state == null) {
- input.addmember(SPConstants.STATE).string(token);
- }
+
+ stateTokenConsumer.accept(profileRequestContext, token);
log.debug("{} State preserved to state token: {}", getLogPrefix(), token);
+ } else{
+ log.debug("{} State value was null, state not preserved", getLogPrefix());
}
- } catch (final IOException e) {
+ } catch (final IOException | RuntimeException e) {
log.warn("{} Exception preserving state to token", getLogPrefix(), e);
if (errorFatal) {
ActionSupport.buildEvent(profileRequestContext, EventIds.IO_ERROR);
diff --git a/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/OIDCSupport.java b/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/OIDCSupport.java
index 7a0c655..0a1b586 100644
--- a/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/OIDCSupport.java
+++ b/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/OIDCSupport.java
@@ -19,7 +19,6 @@ import java.security.SecureRandom;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
-import org.apache.commons.codec.binary.Hex;
import org.opensaml.messaging.context.MessageContext;
import com.nimbusds.openid.connect.sdk.AuthenticationResponse;
@@ -35,7 +34,7 @@ public final class OIDCSupport {
}
/**
- * Generates a random identifier, encoded in Hex..
+ * Generates a secure random identifier, encoded in Hex..
*
* @param length the length of the parameter.
*
@@ -51,29 +50,6 @@ public final class OIDCSupport {
assert nonce != null;
return nonce;
}
-
-
- /**
- * <p>Generate a state parameter from a nonce component and an execution key component.</p>
- *
- * <p>The nonce is separated from the key by a dot e.g. {@literal <nonce>.<keyHex>}.</p>
- *
- * <p>The nonce is assumed to be already encoded in its transmission format e.g. Hex. The key is
- * hex encoded before it is combined with the nonce. The result is assumed URL encoded e.g. inside
- * the allowed set of URI characters or, no character in the state is from the URI reserved set.</p>
- *
- * @param nonce the nonce component.
- * @param key the key component. The key is hex encoded before it is added to the generated state.
- *
- * @return the combined state component.
- */
- @Nonnull public static String generateState(@Nonnull final String nonce, @Nonnull final String key) {
- Constraint.isNotNull(nonce, "NonceHex key can not be null");
- Constraint.isNotNull(key, "Webflow execution key can not be null");
-
- final String keyHex = Hex.encodeHexString(key.getBytes());
- return nonce+"."+keyHex;
- }
/**
* Extracts the state parameter value from an {@link AuthenticationResponse}.
diff --git a/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/ProcessStateCookie.java b/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/ResolveStateCookie.java
similarity index 90%
rename from sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/ProcessStateCookie.java
rename to sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/ResolveStateCookie.java
index 12a14bd..5dafa83 100644
--- a/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/ProcessStateCookie.java
+++ b/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/ResolveStateCookie.java
@@ -41,8 +41,8 @@ import net.shibboleth.sp.messaging.RemotedHttpServletRequestResponseContext;
import net.shibboleth.sp.profile.AbstractApplicationAction;
/**
- * Action that processes a previously issued state cookie submitted with the request and
- * extracts the value for use by subsequent validation steps.
+ * Action that processes a previously issued state cookie, referenced by a state token, submitted with the request and
+ * extracts the value for use by subsequent validation steps. The cookie is immediately unset once retrieved.
*
* <p>The value is set onto the context tree using a custom consumer strategy.</p>
*
@@ -52,13 +52,13 @@ import net.shibboleth.sp.profile.AbstractApplicationAction;
* @event {@link EventIds#PROCEED_EVENT_ID}
* @event {@link EventIds#INVALID_MESSAGE}
*/
-public class ProcessStateCookie extends AbstractApplicationAction {
+public class ResolveStateCookie extends AbstractApplicationAction {
/** Default cookie prefix. */
@Nonnull @NotEmpty public static final String DEFAULT_COOKIE_PREFIX = "_shibsp_req_";
/** Class logger. */
- @Nonnull private final Logger log = LoggerFactory.getLogger(ProcessStateCookie.class);
+ @Nonnull private final Logger log = LoggerFactory.getLogger(ResolveStateCookie.class);
/** Cookie manager. */
@NonnullAfterInit private CookieManager cookieManager;
@@ -83,7 +83,7 @@ public class ProcessStateCookie extends AbstractApplicationAction {
@NonnullBeforeExec private String stateToken;
/** Constructor. */
- public ProcessStateCookie() {
+ public ResolveStateCookie() {
cookiePrefix = DEFAULT_COOKIE_PREFIX;
}
@@ -114,7 +114,7 @@ public class ProcessStateCookie extends AbstractApplicationAction {
/**
* Sets the cookie prefix.
*
- * <p>Defaults to {@link ProcessStateCookie#DEFAULT_COOKIE_PREFIX}.</p>
+ * <p>Defaults to {@link ResolveStateCookie#DEFAULT_COOKIE_PREFIX}.</p>
*
* @param prefix cookie prefix
*/
@@ -199,17 +199,17 @@ public class ProcessStateCookie extends AbstractApplicationAction {
final String cookieName = cookiePrefix + escaper.escape(stateToken);
final String value = cookieManager.getCookieValue(cookieName, null);
+ cookieManager.unsetCookie(cookieName);
+
if (value == null && !errorFatal) {
- log.debug("{} No correlation cookie found for state token '{}'", getLogPrefix(), stateToken);
+ log.debug("{} No cookie found for state token '{}'", getLogPrefix(), stateToken);
return;
} else if (value == null){
- log.warn("{} No correlation cookie found for state token '{}'", getLogPrefix(), stateToken);
+ log.debug("{} No cookie found for state token '{}'", getLogPrefix(), stateToken);
ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_MESSAGE);
return;
}
- cookieManager.unsetCookie(cookieName);
-
if (cookieValueConsumerStrategy != null) {
final Boolean success = cookieValueConsumerStrategy.apply(profileRequestContext, value);
// Treat null as failure
@@ -218,9 +218,9 @@ public class ProcessStateCookie extends AbstractApplicationAction {
ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_MESSAGE);
return;
}
- log.debug("{} Processed correlation cookie for state token '{}'", getLogPrefix(), stateToken);
+ log.debug("{} Resolved cookie for state token '{}'", getLogPrefix(), stateToken);
} else {
- log.warn("{} No cookie value consumer strategy defined, cookie value not processed", getLogPrefix());
+ log.debug("{} No cookie value consumer strategy defined, cookie value not processed", getLogPrefix());
}
} finally {
RemotedHttpServletRequestResponseContext.clearCurrent();
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
index f0c609e..4a3cce5 100644
--- 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
@@ -14,6 +14,7 @@
package net.shibboleth.sp.oidc.profile.impl;
+import java.nio.charset.StandardCharsets;
import java.util.function.BiFunction;
import java.util.function.Predicate;
@@ -33,21 +34,26 @@ import net.shibboleth.shared.component.AbstractIdentifiableInitializableComponen
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.primitive.StringSupport;
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}.
+ * A consumer function that converts the authentication request state data JSON into an
+ * {@link AuthenticationRequestStateData} object and sets it onto an {@link AuthnRequestStateDataContext} within the
+ * inbound message context, along with setting the issuer onto the {@link OIDCPeerEntityContext}.
+ *
+ * <p>The supplied byte array is assumed to represent UTF‑8 JSON, already Base64‑decoded upstream.</p>
+ *
+ * <p>In production use, you should always attempt to unseal the authentication request state, such that it will fail if
+ * it was not sealed. Allowing unsealed authentication request state should only be used for testing.</p>
*/
//TODO this class is complex and more fitting a decoder type mechanism
public class SetAuthenticationRequestToPeerContextConsumer extends AbstractIdentifiableInitializableComponent
- implements BiFunction<ProfileRequestContext, String, Boolean> {
+ implements BiFunction<ProfileRequestContext, byte[], Boolean> {
/** Class logger. */
@Nonnull private final Logger log = LoggerFactory.getLogger(SetAuthenticationRequestToPeerContextConsumer.class);
@@ -125,34 +131,32 @@ public class SetAuthenticationRequestToPeerContextConsumer extends AbstractIdent
/** {@inheritDoc} */
@Override
- public Boolean apply(@Nullable final ProfileRequestContext profileRequestContext, @Nullable final String value) {
+ public Boolean apply(@Nullable final ProfileRequestContext profileRequestContext, @Nullable final byte[] 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;
- }
+ // The value is already base64 decoded by the token manager
+ String valueString = new String(value, StandardCharsets.UTF_8);
// Possibly unseal and convert to class
try {
if (unsealState.test(profileRequestContext) && dataSealer != null) {
- decoded = dataSealer.unwrap(decoded);
+ valueString = dataSealer.unwrap(valueString);
}
- final AuthenticationRequestStateData authnState = objectMapper.readValue(decoded, AuthenticationRequestStateData.class);
+ final AuthenticationRequestStateData authnState =
+ objectMapper.readValue(valueString, 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;
}
+ if (StringSupport.trimOrNull(authnState.getAuthenticationAuthority()) == null) {
+ // Fail early as this is usually terminal
+ log.debug("Authenticating authority could not be recovered");
+ return false;
+ }
// Add identifier to the peer context now we know it
inboundCtx.ensureSubcontext(OIDCPeerEntityContext.class)
.setIdentifier(authnState.getAuthenticationAuthority());
diff --git a/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/SetAuthenticationStateTokenConsumer.java b/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/SetAuthenticationStateTokenConsumer.java
new file mode 100644
index 0000000..80d0357
--- /dev/null
+++ b/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/SetAuthenticationStateTokenConsumer.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.profile.impl;
+
+import java.util.function.BiConsumer;
+
+import javax.annotation.Nonnull;
+
+import org.opensaml.messaging.context.MessageContext;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.slf4j.Logger;
+
+import net.shibboleth.shared.primitive.LoggerFactory;
+import net.shibboleth.sp.oidc.context.AuthnRequestStateDataContext;
+
+/**
+ * A consumer that sets the authentication state token on the {@link AuthnRequestStateDataContext} in the outbound
+ * message context of the {@link ProfileRequestContext}.
+ */
+public class SetAuthenticationStateTokenConsumer implements BiConsumer<ProfileRequestContext, String>{
+
+ /** Class logger. */
+ @Nonnull private final Logger log = LoggerFactory.getLogger(SetAuthenticationStateTokenConsumer.class);
+
+ /** {@inheritDoc} */
+ @Override
+ public void accept(final ProfileRequestContext prc, final String token) {
+ final MessageContext outbound = prc.getOutboundMessageContext();
+ if (outbound == null) {
+ return;
+ }
+ log.trace("Setting authentication state token '{}' onto token context", token);
+ final AuthnRequestStateDataContext stateCtx = outbound.ensureSubcontext(AuthnRequestStateDataContext.class);
+ stateCtx.setToken(token);
+
+ }
+
+}
diff --git a/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/SetCorrelationCookieValueToContextConsumer.java b/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/SetCorrelationCookieValueToContextConsumer.java
new file mode 100644
index 0000000..bf546f1
--- /dev/null
+++ b/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/SetCorrelationCookieValueToContextConsumer.java
@@ -0,0 +1,184 @@
+/*
+ * 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.nio.charset.StandardCharsets;
+import java.util.function.BiFunction;
+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 net.minidev.json.JSONObject;
+import net.minidev.json.JSONValue;
+import net.minidev.json.parser.ParseException;
+import net.shibboleth.shared.annotation.constraint.NonnullAfterInit;
+import net.shibboleth.shared.annotation.constraint.ThreadSafeAfterInit;
+import net.shibboleth.shared.codec.Base64Support;
+import net.shibboleth.shared.codec.DecodingException;
+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.context.CorrelationCookieStateContext;
+
+
+
+/**
+ * Consumer that decodes a correlation cookie value and stores it on a {@link CorrelationCookieStateContext}. The
+ * cookie value is expected to be the raw value of the cookie, decoding happens here.
+ *
+ * <p>The supplied value is expected to represent a JSON object encoded either as:
+ * <ul>
+ * <li>a URL-safe Base64 representation of UTF-8 JSON, or</li>
+ * <li>a URL-safe Base64 representation of UTF-8 JSON sealed using a {@link DataSealer}.</li>
+ * </ul>
+ *
+ * <p>On failure, this consumer returns {@code false}.</p>
+ */
+ at ThreadSafeAfterInit
+public class SetCorrelationCookieValueToContextConsumer extends AbstractIdentifiableInitializableComponent
+ implements BiFunction<ProfileRequestContext, String, Boolean> {
+
+ /** Class logger. */
+ @Nonnull private final Logger log = LoggerFactory.getLogger(SetCorrelationCookieValueToContextConsumer.class);
+
+ /** The strategy to locate the {@link CorrelationCookieStateContext} to operate on. */
+ @NonnullAfterInit
+ private Function<ProfileRequestContext, CorrelationCookieStateContext> correlationCookieStateContextLookupStrategy;
+
+ /** Optional data sealer to use. */
+ @Nullable private DataSealer dataSealer;
+
+ /**
+ * If the dataSealer is provided should it be used to unwrap the correlation cookie state? Defaults to true,
+ * that is, if the dataSealer is provided, always attempt to unwrap the state.
+ */
+ @Nonnull private Predicate<ProfileRequestContext> unsealState;
+
+ /** Constructor.*/
+ public SetCorrelationCookieValueToContextConsumer() {
+ unsealState = PredicateSupport.alwaysTrue();
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected void doInitialize() throws ComponentInitializationException {
+ super.doInitialize();
+
+ if (correlationCookieStateContextLookupStrategy == null) {
+ throw new ComponentInitializationException("CorrelationCookieStateContextLookupStrategy "
+ + "lookup strategy can not be null");
+ }
+ }
+
+ /**
+ * Set the strategy to locate the {@link CorrelationCookieStateContext} to operate on.
+ *
+ * @param strategy The strategy to set.
+ */
+ public void setCorrelationCookieStateContextLookupStrategy(
+ final Function<ProfileRequestContext, CorrelationCookieStateContext> strategy) {
+ checkSetterPreconditions();
+ correlationCookieStateContextLookupStrategy = Constraint.isNotNull(strategy,
+ "CorrelationCookieStateContextLookupStrategy can not 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(final ProfileRequestContext profileRequestContext, final String value) {
+ // Value should be b64 encoded
+ if (value == null || profileRequestContext == null) {
+ return false;
+ }
+ final CorrelationCookieStateContext context =
+ correlationCookieStateContextLookupStrategy.apply(profileRequestContext);
+ if (context == null) {
+ log.debug("Correlation cookie context not found, can not set correlation cookie onto the context");
+ return false;
+ }
+ log.trace("Attempting to set correlation cookie onto the context '{}'", value);
+ Object correlationCookieAsObject = null;
+ try {
+ final byte[] decoded = Base64Support.decodeURLSafe(value);
+ final String decodedString = new String(decoded, StandardCharsets.UTF_8);
+ log.trace("Attempting to set base64 URL decoded correlation cookie '{}'", decodedString);
+ final var localDataSealer = dataSealer;
+ if (localDataSealer != null && unsealState.test(profileRequestContext)) {
+ log.trace("Attempting to unseal correlation cookie");
+ final String unsealed = localDataSealer.unwrap(decodedString);
+ correlationCookieAsObject = JSONValue.parseWithException(unsealed);
+ } else {
+ correlationCookieAsObject = JSONValue.parseWithException(decodedString);
+ }
+
+ } catch (final DataSealerException | DecodingException | ParseException e) {
+ log.warn("Unable to process correlation cookie, was it sealed and you are not unsealing it?"
+ , e);
+ return false;
+ }
+ log.trace("Decoded correlation cookie as '{}'", correlationCookieAsObject);
+ if (correlationCookieAsObject instanceof final JSONObject json) {
+ context.setValue(json);
+ } else {
+ log.error("Decoded correlation cookie was not a JSON object");
+ return false;
+ }
+
+ return true;
+
+ }
+
+}
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
deleted file mode 100644
index 2d7b7f1..0000000
--- a/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/SetNonceValueToTokenContextConsumer.java
+++ /dev/null
@@ -1,53 +0,0 @@
-/*
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package net.shibboleth.sp.oidc.profile.impl;
-
-import java.util.function.BiFunction;
-
-import javax.annotation.Nonnull;
-
-import org.opensaml.profile.context.ProfileRequestContext;
-import org.slf4j.Logger;
-
-import net.shibboleth.shared.net.URISupport;
-import net.shibboleth.shared.primitive.LoggerFactory;
-import net.shibboleth.sp.context.AgentRequestContext;
-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 BiFunction<ProfileRequestContext, String, Boolean> {
-
- /** Class logger. */
- @Nonnull private final Logger log = LoggerFactory.getLogger(SetNonceValueToTokenContextConsumer.class);
-
- /** {@inheritDoc} */
- @Override
- public Boolean apply(final ProfileRequestContext profileRequestContext, final String value) {
-
- final String decoded = URISupport.doURLDecode(value);
-
- log.debug("Nonce '{}' set onto token context", decoded);
-
- profileRequestContext.ensureSubcontext(AgentRequestContext.class)
- .ensureSubcontext(TokenConsumerContext.class).setMessageCorrelationID(decoded);
- return true;
-
- }
-
-}
diff --git a/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/StateLookupStrategy.java b/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/StateLookupStrategy.java
index 979bfcb..053b523 100644
--- a/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/StateLookupStrategy.java
+++ b/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/StateLookupStrategy.java
@@ -14,40 +14,92 @@
package net.shibboleth.sp.oidc.profile.impl;
+import java.nio.charset.StandardCharsets;
+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.opensaml.profile.context.navigate.ParentProfileRequestContextLookup;
import org.slf4j.Logger;
-import com.nimbusds.jose.util.StandardCharset;
-
import net.minidev.json.JSONObject;
import net.minidev.json.JSONValue;
import net.shibboleth.oidc.profile.core.StateToken;
import net.shibboleth.shared.codec.Base64Support;
import net.shibboleth.shared.codec.EncodingException;
+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.ddf.DDF;
import net.shibboleth.sp.oidc.profile.OIDCConstants;
import net.shibboleth.sp.profile.SPConstants;
/**
* A strategy function that constructs a Base64URL encoded JSON object containing a state value from the input
- * {@link DDF} and a cryptographically secure nonce for CSRF protection.
+ * {@link DDF} and a cryptographically secure request forgery protection value for CSRF protection.
*
* <p>The JSON Object includes the following:</p>
* <ul>
* <li><b>state</b>: The value of {@code SPConstants.STATE} from the input DDF.</li>
- * <li><b>nonce</b>: A securely generated random string (32 characters).</li>
+ * <li><b>rfp</b>: A securely generated random string (32 characters).</li>
* </ul>
- *
- *
*/
public class StateLookupStrategy extends AbstractAgentAndRelyingPartyContextLookupFunction<StateToken> {
+ /** Lookup function for parent ProfileRequestContext. */
+ @Nonnull private static final ParentProfileRequestContextLookup<MessageContext> PRC_LOOKUP
+ = new ParentProfileRequestContextLookup<>();
+
/** Class logger. */
@Nonnull
- private final Logger log = LoggerFactory.getLogger(StateLookupStrategy.class);
+ private final Logger log = LoggerFactory.getLogger(StateLookupStrategy.class);
+
+ /** 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 StateLookupStrategy() {
+ sealState = PredicateSupport.alwaysTrue();
+ }
+
+ /**
+ * Sets {@link DataSealer} to use.
+ *
+ * @param sealer data sealer
+ */
+ public void setDataSealer(@Nullable final DataSealer sealer) {
+ 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) {
+ 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) {
+ sealState = flag ? PredicateSupport.alwaysTrue() : PredicateSupport.alwaysFalse();
+ }
+
/** {@inheritDoc} */
@Override
@@ -56,19 +108,36 @@ public class StateLookupStrategy extends AbstractAgentAndRelyingPartyContextLook
if (input == null) {
return null;
}
+
final JSONObject stateObject = new JSONObject();
stateObject.appendField(OIDCConstants.STATE_FIELD, input.getmember(SPConstants.STATE).string());
- stateObject.appendField(OIDCConstants.NONCE_FIELD, OIDCSupport.generateRandom(32));
+ stateObject.appendField(OIDCConstants.RFP_FIELD, OIDCSupport.generateRandom(32));
final String stateJsonString = stateObject.toJSONString(JSONValue.COMPRESSION);
if (stateJsonString == null || stateJsonString.isEmpty()) {
return null;
}
try {
- final byte[] stateJsonAsBytes = stateJsonString.getBytes(StandardCharset.UTF_8);
+ final DataSealer localDataSealer = dataSealer;
+ if (localDataSealer != null && sealState.test(PRC_LOOKUP.apply(messageCtx))) {
+ log.trace("Sealing OAuth state");
+ final String wrapped = localDataSealer.wrap(stateJsonString);
+ final byte[] wrappedAsBytes = wrapped.getBytes(StandardCharsets.UTF_8);
+ if (wrappedAsBytes == null) {
+ log.error("Unable to seal OAuth state, wrapped bytes are empty");
+ return null;
+ }
+ // The data sealer base64 encodes the sealed output, we base64 URL encode the base64 encoded output
+ // for transport in the URL.
+ return new StateToken(Base64Support.encodeURLSafe(wrappedAsBytes), stateObject);
+ }
+ log.warn("OAuth state was NOT sealed, either DataSealer is not configured or "
+ + "sealing predicate returned false. Sealing should be enabled in production");
+
+ final byte[] stateJsonAsBytes = stateJsonString.getBytes(StandardCharsets.UTF_8);
assert stateJsonAsBytes != null;
final String serializedValue = Base64Support.encodeURLSafe(stateJsonAsBytes);
return new StateToken(serializedValue, stateObject);
- } catch (final EncodingException e) {
+ } catch (final EncodingException | DataSealerException e) {
log.error("Unable to generate OAuth state", e);
return null;
}
diff --git a/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/ValidateResponseState.java b/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/ValidateResponseState.java
index 422e4a5..a3a5aef 100644
--- a/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/ValidateResponseState.java
+++ b/sp-oidc-impl/src/main/java/net/shibboleth/sp/oidc/profile/impl/ValidateResponseState.java
@@ -28,67 +28,81 @@ import net.shibboleth.shared.component.ComponentInitializationException;
import net.shibboleth.shared.logic.Constraint;
import net.shibboleth.shared.primitive.LoggerFactory;
import net.shibboleth.shared.primitive.StringSupport;
-import net.shibboleth.sp.context.AgentRequestContext;
-import net.shibboleth.sp.context.TokenConsumerContext;
import net.shibboleth.sp.profile.AbstractApplicationAction;
/**
- * An action that validates that the nonce in the OAuth 2.0 state parameter in the response matches that recovered from
- * the stored nonce.
+ * An action that validates that the request forgery protection (RFP) nonce in the OAuth 2.0 state parameter in the
+ * response matches that recovered from the stored RFP nonce in the correlation cookie.
*/
public class ValidateResponseState extends AbstractApplicationAction {
/** Class logger. */
@Nonnull private final Logger log = LoggerFactory.getLogger(ValidateResponseState.class);
- /** Lookup strategy for nonce value stored in the OAuth state parameter. */
- @NonnullAfterInit private Function<ProfileRequestContext,String> nonceTokenLookupStrategy;
+ /** Lookup strategy for the RFP value stored in the OAuth state parameter. */
+ @NonnullAfterInit private Function<ProfileRequestContext,String> rfpTokenFromOAuthStateLookupStrategy;
+
+ /**
+ * Lookup strategy to obtain the RFP value stored against the users session in a cookie. This represents
+ * the RFP value that was sent in the initial request.
+ */
+ @NonnullAfterInit private Function<ProfileRequestContext, String> rfpFromCookieLookupStrategy;
/** {@inheritDoc} */
@Override
protected void doInitialize() throws ComponentInitializationException {
super.doInitialize();
- if (nonceTokenLookupStrategy == null) {
- throw new ComponentInitializationException("Nonce token lookup strategy cannot be null");
+ if (rfpFromCookieLookupStrategy == null) {
+ throw new ComponentInitializationException("Request forgery token lookup strategy from cookie "
+ + "cannot be null");
+ }
+ if (rfpTokenFromOAuthStateLookupStrategy == null) {
+ throw new ComponentInitializationException("Request forgery token lookup strategy from OAuth state"
+ + " cannot be null");
}
}
/**
- * Sets the lookup strategy to obtain the nonce value stored in the OAuth state
+ * Sets the lookup strategy to obtain the RFP value stored in the OAuth state
* parameter.
*
* @param strategy
* lookup strategy
*/
- public void setNonceTokenLookupStrategy(final Function<ProfileRequestContext, String> strategy) {
+ public void setRfpTokenFromOAuthStateLookupStrategy(final Function<ProfileRequestContext, String> strategy) {
checkSetterPreconditions();
- nonceTokenLookupStrategy = Constraint.isNotNull(strategy,
- "NonceTokenLookupStrategy can not be null");
+ rfpTokenFromOAuthStateLookupStrategy = Constraint.isNotNull(strategy,
+ "RFP token from oAuth state lookup strategy can not be null");
+ }
+ /**
+ * Set the lookup strategy to obtain the RFP value stored against the users session in a cookie.
+ *
+ * @param strategy lookup strategy
+ */
+ public void setRfpFromCookieLookupStrategy(final Function<ProfileRequestContext, String> strategy) {
+ checkSetterPreconditions();
+ rfpFromCookieLookupStrategy = Constraint.isNotNull(strategy,
+ "RFP from cookie lookup strategy can not be null");
}
@Override
protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
- final AgentRequestContext agentRequestContext = ensureAgentRequestContext();
- // Using this to retrieve the nonce value stored earlier
- final String nonceFromCorrelationCookie =
- agentRequestContext.ensureSubcontext(TokenConsumerContext.class).getMessageCorrelationID();
-
- final String nonceFromStateParam = nonceTokenLookupStrategy.apply(profileRequestContext);
-
- // We always require state, otherwise we can not recover the values we need
- if (StringSupport.trimOrNull(nonceFromCorrelationCookie) == null ||
- StringSupport.trimOrNull(nonceFromStateParam) == null) {
- log.error("{} The state parameter was not present in both the request and response, "
+ final String rfpFromCorrelationCookie = rfpFromCookieLookupStrategy.apply(profileRequestContext);
+ final String rfpFromStateParam = rfpTokenFromOAuthStateLookupStrategy.apply(profileRequestContext);
+
+ if (StringSupport.trimOrNull(rfpFromCorrelationCookie) == null ||
+ StringSupport.trimOrNull(rfpFromStateParam) == null) {
+ log.error("{} Required OAuth state data was missing from the request or response, "
+ "state is required",getLogPrefix());
ActionSupport.buildEvent(profileRequestContext, EventIds.MESSAGE_PROC_ERROR);
return;
}
- assert nonceFromCorrelationCookie != null;
- if (!nonceFromCorrelationCookie.equals(nonceFromStateParam)) {
- log.error("{} Request state '{}' did not match response state '{}', has it been tampered with!",
- getLogPrefix(), nonceFromCorrelationCookie, nonceFromStateParam);
+ assert rfpFromCorrelationCookie != null;
+ if (!rfpFromCorrelationCookie.equals(rfpFromStateParam)) {
+ log.error("{} Request state did not match response state, has it been tampered with!",
+ getLogPrefix());
ActionSupport.buildEvent(profileRequestContext, EventIds.MESSAGE_PROC_ERROR);
return;
}
diff --git a/sp-oidc-impl/src/test/java/net/shibboleth/sp/oidc/profile/impl/IssueStateCookieTest.java b/sp-oidc-impl/src/test/java/net/shibboleth/sp/oidc/profile/impl/IssueStateCookieTest.java
index 4232079..e99bd2a 100644
--- a/sp-oidc-impl/src/test/java/net/shibboleth/sp/oidc/profile/impl/IssueStateCookieTest.java
+++ b/sp-oidc-impl/src/test/java/net/shibboleth/sp/oidc/profile/impl/IssueStateCookieTest.java
@@ -92,8 +92,7 @@ public class IssueStateCookieTest extends BaseAgplicationActionTest {
}
});
- action.setErrorFatal(true);
- action.initialize();
+ action.setErrorFatal(true);
input = new DDF(null).structure();
arc.setInput(input);
@@ -111,17 +110,24 @@ public class IssueStateCookieTest extends BaseAgplicationActionTest {
@DataProvider
Object[][] correlationData() {
return new Object[][] {
- new Object[] { null, TEST_VALUE},
- new Object[] { TEST_STATE, null},
+ new Object[] { null, TEST_VALUE},
new Object[] { TEST_STATE, TEST_VALUE},
};
}
@Test(dataProvider="correlationData")
- public void testAction(final String state, final String id) {
+ public void testAction(final String state, final String id) throws ComponentInitializationException {
+ action.initialize();
evaluateAction(state, id);
}
+ @Test
+ public void testAction_NullValue() throws ComponentInitializationException {
+ action.setErrorFatal(false);
+ action.initialize();
+ evaluateAction(TEST_STATE, null);
+ }
+
private void evaluateAction(final String state, final String id) {
stateValue = id;
if (state != null) {
@@ -151,7 +157,7 @@ public class IssueStateCookieTest extends BaseAgplicationActionTest {
@Test
public void testPurge() throws ComponentInitializationException, DecodingException, InterruptedException {
-
+ action.initialize();
final List<Cookie> cookies = new ArrayList<>(12);
for (int i = 0; i < 12; ++i) {
cookies.add(new Cookie(IssueStateCookie.DEFAULT_COOKIE_PREFIX + i, "foo" + i));
diff --git a/sp-oidc-impl/src/test/java/net/shibboleth/sp/oidc/profile/impl/ProcessStateCookieTest.java b/sp-oidc-impl/src/test/java/net/shibboleth/sp/oidc/profile/impl/ResolveStateCookieTest.java
similarity index 96%
rename from sp-oidc-impl/src/test/java/net/shibboleth/sp/oidc/profile/impl/ProcessStateCookieTest.java
rename to sp-oidc-impl/src/test/java/net/shibboleth/sp/oidc/profile/impl/ResolveStateCookieTest.java
index 0d10a62..756ccee 100644
--- a/sp-oidc-impl/src/test/java/net/shibboleth/sp/oidc/profile/impl/ProcessStateCookieTest.java
+++ b/sp-oidc-impl/src/test/java/net/shibboleth/sp/oidc/profile/impl/ResolveStateCookieTest.java
@@ -31,11 +31,11 @@ import net.shibboleth.shared.net.CookieManager;
import net.shibboleth.sp.profile.impl.BaseAgplicationActionTest;
/**
- * Tests for {@link ProcessStateCookie}.
+ * Tests for {@link ResolveStateCookie}.
*/
-public class ProcessStateCookieTest extends BaseAgplicationActionTest {
+public class ResolveStateCookieTest extends BaseAgplicationActionTest {
- private ProcessStateCookie action;
+ private ResolveStateCookie action;
private CookieManager cookieManager;
@@ -43,7 +43,7 @@ public class ProcessStateCookieTest extends BaseAgplicationActionTest {
@BeforeMethod
public void BeforeMethod() throws ComponentInitializationException {
super.beforeMethod();
- action = new ProcessStateCookie();
+ action = new ResolveStateCookie();
cookieManager = Mockito.mock(CookieManager.class);
action.setCookieManager(cookieManager);
}
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
index 65a3eaa..9a17fcc 100644
--- 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
@@ -18,6 +18,8 @@ import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertNotNull;
import static org.testng.Assert.assertNull;
+import java.nio.charset.StandardCharsets;
+
import org.mockito.Mockito;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;
@@ -46,6 +48,9 @@ public class SetAuthenticationRequestToPeerContextConsumerTest extends BaseAgpli
/** The serialized form of the authnState.*/
private String authnStateSerialized;
+ /** The serialized form of the authnState as UTF-8 encoded bytes.*/
+ private byte[] authnStateSerializedAsBytes;
+
@Override
@BeforeMethod
public void beforeMethod() throws ComponentInitializationException {
@@ -63,6 +68,7 @@ public class SetAuthenticationRequestToPeerContextConsumerTest extends BaseAgpli
function.setObjectMapper(mapper);
try {
authnStateSerialized = mapper.writeValueAsString(authnState);
+ authnStateSerializedAsBytes = authnStateSerialized.getBytes(StandardCharsets.UTF_8);
} catch (final JsonProcessingException e) {
throw new ComponentInitializationException(e);
}
@@ -73,7 +79,7 @@ public class SetAuthenticationRequestToPeerContextConsumerTest extends BaseAgpli
public void testSuccess() throws Exception {
function.initialize();
- function.apply(prc, authnStateSerialized);
+ function.apply(prc, authnStateSerializedAsBytes);
final var inboundMsgCtx = prc.getInboundMessageContext();
assertNotNull(inboundMsgCtx);
@@ -101,7 +107,7 @@ public class SetAuthenticationRequestToPeerContextConsumerTest extends BaseAgpli
function.setDataSealer(sealer);
function.initialize();
- function.apply(prc, authnStateSerialized);
+ function.apply(prc, authnStateSerializedAsBytes);
final var inboundMsgCtx = prc.getInboundMessageContext();
assertNotNull(inboundMsgCtx);
@@ -125,7 +131,7 @@ public class SetAuthenticationRequestToPeerContextConsumerTest extends BaseAgpli
public void testFail_BadJSONAuthnState() throws Exception {
function.initialize();
- function.apply(prc, "bad");
+ function.apply(prc, "bad".getBytes(StandardCharsets.UTF_8));
final var inboundMsgCtx = prc.getInboundMessageContext();
assertNotNull(inboundMsgCtx);
--
To stop receiving notification emails like this one, please contact
the administrator of this repository.
More information about the commits
mailing list