[java-idp-oidc] 01/02: JOIDC-201 - Support for OAuth 2.0 Demonstrating Proof of Possession (DPoP)
Henri Mikkonen
henri.mikkonen at iki.fi
Mon May 13 11:49:40 UTC 2024
This is an automated email from the git hooks/post-receive script.
hjmikkon pushed a commit to branch main
in repository java-idp-oidc.
View the commit online:
http://git.shibboleth.net/view/?p=java-idp-oidc.git;a=commit;h=b2c844652cba725382293ad0f65129a44c03380c
commit b2c844652cba725382293ad0f65129a44c03380c
Author: Henri Mikkonen <henri.mikkonen at iki.fi>
AuthorDate: Mon May 13 14:47:36 2024 +0300
JOIDC-201 - Support for OAuth 2.0 Demonstrating Proof of Possession (DPoP)
https://shibboleth.atlassian.net/browse/JOIDC-201
Initial, still very incomplete commit. WIP.
- DPoP Proof JWK thumbprint fields for OIDCAuthenticationResponseContext and TokenClaimsSet
- OIDCAuthenticationResponseContext.setAccessToken(..) builds DPoPAccessToken if thumbprint is set
- PAR/authorize -flows support setting the thumbprint via dpop_jkt -parameter
- StoreDPoPProofKeyThumbprint action
- Requirement for the parameter may be set via profile config, defaults to false
- New OAuth2DPoPProofContext to carry information about DPoP Proof JWT
- New oauth2/dpop-proof-validation flow to perform DPoP Proof JWT validation
- Populates security parameters context under inboud / OAuth2DPoPProofContext
- Signature validation paramters allow validation solely against token-derived credentials
- configuration via profile configuration settings
- Currently called by oidc/abstract-api flow right before client authentication
- Final position in the sequence for the flow TBD
---
.../context/OIDCAuthenticationResponseContext.java | 38 +++-
.../messaging/context/OAuth2DPoPProofContext.java | 74 ++++++++
.../DefaultDPoPProofThumbprintLookupFunction.java | 48 +++++
.../DefaultRequestDPoPJktLookupFunction.java | 63 +++++++
.../op/token/support/AccessTokenClaimsSet.java | 1 +
.../op/token/support/RefreshTokenClaimsSet.java | 1 +
.../oidc/op/token/support/TokenClaimsSet.java | 41 ++++-
.../op/oauth2/profile/impl/BuildAccessToken.java | 3 +-
.../profile/impl/InitializeDPoPProofContext.java | 202 +++++++++++++++++++++
.../SetAuthorizationCodeToResponseContext.java | 1 +
.../profile/impl/StoreDPoPProofKeyThumbprint.java | 142 +++++++++++++++
.../op/oauth2/profile/impl/ValidateDPoPProof.java | 158 ++++++++++++++++
.../impl/SetRefreshTokenToResponseContext.java | 1 +
.../plugin/oidc/op/profile/impl/ValidateGrant.java | 1 +
.../dpop-proof-validation-beans.xml | 87 +++++++++
.../dpop-proof-validation-flow.xml | 17 ++
.../pushed-authorization-beans.xml | 3 +
.../pushed-authorization-flow.xml | 1 +
.../oidc/abstract-api/oidc-abstract-api-flow.xml | 4 +-
.../idp/flows/oidc/authorize/authorize-beans.xml | 3 +
.../idp/flows/oidc/authorize/authorize-flow.xml | 1 +
.../idp/service/relying-party/postconfig.xml | 58 +++++-
.../oidc/op/profile/flow/AbstractOidcFlowTest.java | 17 ++
.../oidc/op/profile/flow/AuthorizeFlowTest.java | 56 ++++++
.../op/profile/flow/PushedAuthorizeFlowTest.java | 38 ++++
25 files changed, 1052 insertions(+), 7 deletions(-)
diff --git a/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/messaging/context/OIDCAuthenticationResponseContext.java b/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/messaging/context/OIDCAuthenticationResponseContext.java
index a8a93ec8..b4065096 100644
--- a/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/messaging/context/OIDCAuthenticationResponseContext.java
+++ b/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/messaging/context/OIDCAuthenticationResponseContext.java
@@ -29,6 +29,7 @@ import com.nimbusds.oauth2.sdk.AuthorizationCode;
import com.nimbusds.oauth2.sdk.Scope;
import com.nimbusds.oauth2.sdk.token.AccessToken;
import com.nimbusds.oauth2.sdk.token.BearerAccessToken;
+import com.nimbusds.oauth2.sdk.token.DPoPAccessToken;
import com.nimbusds.oauth2.sdk.token.RefreshToken;
import com.nimbusds.openid.connect.sdk.OIDCClaimsRequest;
import com.nimbusds.openid.connect.sdk.claims.ACR;
@@ -131,6 +132,9 @@ public class OIDCAuthenticationResponseContext extends BaseContext {
/** Whether request object has already been validated. */
private boolean requestObjectValidated = false;
+ /** DPoP Proof JWK thumbprint. */
+ @Nullable private String dpopProofJwkThumbprint;
+
/** Constructor. */
public OIDCAuthenticationResponseContext() {
validatedAudience = new ArrayList<>();
@@ -469,7 +473,15 @@ public class OIDCAuthenticationResponseContext extends BaseContext {
*/
public void setAccessToken(@Nullable final String token, @Nonnull final Duration lifeTime,
@Nullable final Scope scope) {
- accessToken = token == null ? null : new BearerAccessToken(token, lifeTime.getSeconds(), scope);
+ if (token == null) {
+ accessToken = null;
+ } else {
+ if (getDpopProofJwkThumbprint() == null) {
+ accessToken = new BearerAccessToken(token, lifeTime.getSeconds(), scope);
+ } else {
+ accessToken = new DPoPAccessToken(token, lifeTime.getSeconds(), scope);
+ }
+ }
}
/**
@@ -573,5 +585,27 @@ public class OIDCAuthenticationResponseContext extends BaseContext {
public void setRequestObjectValidated(final boolean flag) {
requestObjectValidated = flag;
}
-
+
+ /**
+ * Get the DPoP Proof JWK thumbprint.
+ *
+ * @return DPoP Proof JWK thumbprint
+ *
+ * @since 4.2.0
+ */
+ @Nullable
+ public String getDpopProofJwkThumbprint() {
+ return dpopProofJwkThumbprint;
+ }
+
+ /**
+ * Set the DPoP Proof JWK thumbprint.
+ *
+ * @param jkt DPoP Proof JWK thumbprint
+ *
+ * @since 4.2.0
+ */
+ public void setDpopProofJwkThumbprint(@Nullable final String jkt) {
+ dpopProofJwkThumbprint = jkt;
+ }
}
\ No newline at end of file
diff --git a/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/oauth2/messaging/context/OAuth2DPoPProofContext.java b/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/oauth2/messaging/context/OAuth2DPoPProofContext.java
new file mode 100644
index 00000000..355f0f1f
--- /dev/null
+++ b/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/oauth2/messaging/context/OAuth2DPoPProofContext.java
@@ -0,0 +1,74 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.idp.plugin.oidc.op.oauth2.messaging.context;
+
+import javax.annotation.Nullable;
+
+import org.opensaml.messaging.context.BaseContext;
+import org.opensaml.messaging.context.MessageContext;
+
+import com.nimbusds.jwt.SignedJWT;
+
+/**
+ * Subcontext carrying information for an OAuth2 DPoP Proof JWT.
+ *
+ * <p>This context appears as a subcontext of a {@link MessageContext}.</p>
+ *
+ * @since 4.2.0
+ */
+public class OAuth2DPoPProofContext extends BaseContext {
+
+ /** DPoP Proof JWT. */
+ @Nullable private SignedJWT dpopProof;
+
+ /** Validated DPoP Proof JWT JWK thumbprint. */
+ @Nullable private String dPopProofThumbprint;
+
+ /**
+ * Get the DPoP proof JWT.
+ *
+ * @return DPoP proof JWT.
+ */
+ @Nullable public SignedJWT getDpopProof() {
+ return dpopProof;
+ }
+
+ /**
+ * Set the DPoP proof JWT.
+ *
+ * @param jwt DPoP proof JWT
+ */
+ public void setDpopProof(@Nullable final SignedJWT jwt) {
+ dpopProof = jwt;
+ }
+
+ /**
+ * Get the validated DPoP Proof JWT JWK thumbprint.
+ *
+ * @return validated DPoP Proof JWT JWK thumbprint.
+ */
+ @Nullable public String getValidatedDpopProofThumbprint() {
+ return dPopProofThumbprint;
+ }
+
+ /**
+ * Set the validated DPoP Proof JWT JWK thumbprint.
+ *
+ * @param thumbprint validated DPoP Proof JWT JWK thumbprint.
+ */
+ public void setValidatedDpopProofThumbprint(@Nullable final String thumbprint) {
+ dPopProofThumbprint = thumbprint;
+ }
+}
\ No newline at end of file
diff --git a/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/context/navigate/DefaultDPoPProofThumbprintLookupFunction.java b/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/context/navigate/DefaultDPoPProofThumbprintLookupFunction.java
new file mode 100644
index 00000000..8afe8a6d
--- /dev/null
+++ b/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/context/navigate/DefaultDPoPProofThumbprintLookupFunction.java
@@ -0,0 +1,48 @@
+/*
+ * 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.idp.plugin.oidc.op.profile.context.navigate;
+
+import javax.annotation.Nullable;
+
+import org.opensaml.messaging.context.MessageContext;
+import org.opensaml.messaging.context.navigate.ContextDataLookupFunction;
+import org.opensaml.profile.context.ProfileRequestContext;
+
+import net.shibboleth.idp.plugin.oidc.op.oauth2.messaging.context.OAuth2DPoPProofContext;
+import net.shibboleth.shared.component.AbstractIdentifiableInitializableComponent;
+
+/**
+ * A function that returns validated DPoP proof JWK thumbprint if stored in {@link OAuth2DPoPProofContext}.
+ *
+ * @since 4.2.0
+ */
+public class DefaultDPoPProofThumbprintLookupFunction extends AbstractIdentifiableInitializableComponent
+ implements ContextDataLookupFunction<ProfileRequestContext, String> {
+
+ /** {@inheritDoc} */
+ @Override
+ public String apply(@Nullable final ProfileRequestContext profileRequestContext) {
+ if (profileRequestContext != null) {
+ final MessageContext inboundContext = profileRequestContext.getInboundMessageContext();
+ if (inboundContext != null) {
+ final OAuth2DPoPProofContext proofContext = inboundContext.getSubcontext(OAuth2DPoPProofContext.class);
+ if (proofContext != null) {
+ return proofContext.getValidatedDpopProofThumbprint();
+ }
+ }
+ }
+ return null;
+ }
+}
\ No newline at end of file
diff --git a/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/context/navigate/DefaultRequestDPoPJktLookupFunction.java b/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/context/navigate/DefaultRequestDPoPJktLookupFunction.java
new file mode 100644
index 00000000..68bc7463
--- /dev/null
+++ b/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/context/navigate/DefaultRequestDPoPJktLookupFunction.java
@@ -0,0 +1,63 @@
+/*
+ * 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.idp.plugin.oidc.op.profile.context.navigate;
+
+import java.text.ParseException;
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.slf4j.Logger;
+
+import com.nimbusds.jwt.JWT;
+import com.nimbusds.oauth2.sdk.AuthorizationRequest;
+
+import net.shibboleth.shared.primitive.LoggerFactory;
+
+/**
+ * A function that returns dpop_jkt value of the authorization request via a lookup function. This default lookup
+ * locates dpop_jkt from OAuth authorization request if available. If information is not available, null is
+ * returned. If there is dpop_jkt in request object it is used instead of dpop_jkt parameter.
+ *
+ * @since 4.2.0
+ */
+public class DefaultRequestDPoPJktLookupFunction extends AbstractAuthorizationRequestLookupFunction<String> {
+
+ /** Class logger. */
+ @Nonnull
+ private Logger log = LoggerFactory.getLogger(DefaultRequestDPoPJktLookupFunction.class);
+
+ /** {@inheritDoc} */
+ @Nullable protected String doLookup(@Nonnull final AuthorizationRequest req) {
+ final JWT requestObject = getRequestObject();
+ try {
+ if (requestObject != null && requestObject.getJWTClaimsSet().getClaim("dpop_jkt") != null) {
+ final Object thumbprint = requestObject.getJWTClaimsSet().getClaim("dpop_jkt");
+ if (thumbprint instanceof String string) {
+ return string;
+ } else {
+ log.error("dpop_jkt claim is not of expected type");
+ return null;
+ }
+
+ }
+ } catch (final ParseException e) {
+ log.error("Unable to parse dpop_jkt from request object dpop_jkt value");
+ return null;
+ }
+ return req.getDPoPJWKThumbprintConfirmation() == null ?
+ null : req.getDPoPJWKThumbprintConfirmation().getValue().toString();
+ }
+
+}
\ No newline at end of file
diff --git a/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/token/support/AccessTokenClaimsSet.java b/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/token/support/AccessTokenClaimsSet.java
index 53fcb0b0..44d6d15f 100644
--- a/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/token/support/AccessTokenClaimsSet.java
+++ b/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/token/support/AccessTokenClaimsSet.java
@@ -174,6 +174,7 @@ public final class AccessTokenClaimsSet extends TokenClaimsSet {
setConsentEnabled(existing.isConsentEnabled());
setRootTokenIdentifier(existing.getRootTokenIdentifier());
setSessionIdentifier(existing.getSessionIdentifier());
+ setDpopProofJwkThumbprint(existing.getDpopProofJwkThumbprint());
}
/**
diff --git a/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/token/support/RefreshTokenClaimsSet.java b/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/token/support/RefreshTokenClaimsSet.java
index ad276d59..43b5299a 100644
--- a/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/token/support/RefreshTokenClaimsSet.java
+++ b/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/token/support/RefreshTokenClaimsSet.java
@@ -178,6 +178,7 @@ public final class RefreshTokenClaimsSet extends TokenClaimsSet {
setConsentedClaims(existing.getConsentedClaims());
setConsentEnabled(existing.isConsentEnabled());
setSessionIdentifier(existing.getSessionIdentifier());
+ setDpopProofJwkThumbprint(existing.getDpopProofJwkThumbprint());
}
/**
diff --git a/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/token/support/TokenClaimsSet.java b/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/token/support/TokenClaimsSet.java
index ea9d1a7f..c6ca4c5f 100644
--- a/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/token/support/TokenClaimsSet.java
+++ b/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/token/support/TokenClaimsSet.java
@@ -137,6 +137,9 @@ public class TokenClaimsSet {
/** Identifier for the session id. */
@Nonnull @NotEmpty public static final String KEY_SESSION_ID = "sid";
+ /** Identifier for the DPoP Proof JWK thumbprint. */
+ @Nonnull @NotEmpty public static final String KEY_DPOP_PROOF_JWK_THUMBPRINT = "jkt";
+
/** Claims set for the claim. */
@Nullable private JWTClaimsSet tokenClaimsSet;
@@ -230,6 +233,9 @@ public class TokenClaimsSet {
if (tokenClaimsSet.getClaims().containsKey(KEY_CODE_CHALLENGE)) {
tokenClaimsSet.getStringClaim(KEY_CODE_CHALLENGE);
}
+ if (tokenClaimsSet.getClaims().containsKey(KEY_DPOP_PROOF_JWK_THUMBPRINT)) {
+ tokenClaimsSet.getStringClaim(KEY_DPOP_PROOF_JWK_THUMBPRINT);
+ }
}
// Checkstyle: CyclomaticComplexity ON
@@ -645,6 +651,21 @@ public class TokenClaimsSet {
return (String) tokenClaimsSet.getClaim(KEY_SESSION_ID);
}
+ /**
+ * Get the DPoP Proof JWK thumbprint.
+ *
+ * @return the DPoP Proof JWK thumbprint.
+ *
+ * @since 4.2.0
+ */
+ @Nullable public String getDpopProofJwkThumbprint() {
+ final JWTClaimsSet tokenClaimsSet = assertedClaimsSet();
+ if (tokenClaimsSet.getClaim(KEY_DPOP_PROOF_JWK_THUMBPRINT) == null) {
+ return null;
+ }
+ return (String) tokenClaimsSet.getClaim(KEY_DPOP_PROOF_JWK_THUMBPRINT);
+ }
+
/**
* Abstract builder to extend builders from that are instantiating claims sets extending TokenClaimsSet.
*
@@ -726,6 +747,9 @@ public class TokenClaimsSet {
/** Session identifier. */
@Nullable protected String sessionId;
+ /** DPoP Proof JWK thumbprint. */
+ @Nullable protected String dpopProofJwkThumbprint;
+
/** Default constructor. */
protected Builder() {
audience = CollectionSupport.emptyList();
@@ -785,7 +809,8 @@ public class TokenClaimsSet {
.claim(KEY_CODE_CHALLENGE, codeChallenge)
.claim(KEY_CONSENT_ENABLED, consentEnabled)
.claim(KEY_ROOT_JTI, rootTokenId)
- .claim(KEY_SESSION_ID, sessionId);
+ .claim(KEY_SESSION_ID, sessionId)
+ .claim(KEY_DPOP_PROOF_JWK_THUMBPRINT, dpopProofJwkThumbprint);
customClaims.forEach((n,v) -> {
if (n != null) {
@@ -1174,6 +1199,20 @@ public class TokenClaimsSet {
return this;
}
+ /**
+ * Set DPoP Proof JWK thumbprint.
+ *
+ * @param jkt DPoP Proof JWK thumbprint
+ *
+ * @return the builder
+ *
+ * @since 4.2.0
+ */
+ public Builder<T> setDpopProofJwkThumbprint(@Nullable final String jkt) {
+ dpopProofJwkThumbprint = jkt;
+ return this;
+ }
+
/**
* Builds claims set.
*
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/BuildAccessToken.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/BuildAccessToken.java
index 47629f09..b56388dd 100644
--- a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/BuildAccessToken.java
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/BuildAccessToken.java
@@ -536,7 +536,8 @@ public class BuildAccessToken extends AbstractOIDCResponseAction {
.setDlClaimsUI(claimsUI)
.setConsentedClaims(consented)
.setConsentEnabled(consentEnabledPredicate.test(profileRequestContext))
- .setSessionIdentifier(responseCtx.getSessionId());
+ .setSessionIdentifier(responseCtx.getSessionId())
+ .setDpopProofJwkThumbprint(responseCtx.getDpopProofJwkThumbprint());
assert subjectCtx != null;
final String principal = subjectCtx.getPrincipalName();
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/InitializeDPoPProofContext.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/InitializeDPoPProofContext.java
new file mode 100644
index 00000000..e2bc3afd
--- /dev/null
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/InitializeDPoPProofContext.java
@@ -0,0 +1,202 @@
+/*
+ * 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.idp.plugin.oidc.op.oauth2.profile.impl;
+
+import java.text.ParseException;
+import java.util.Collections;
+import java.util.Enumeration;
+import java.util.List;
+import java.util.function.Predicate;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.messaging.encoder.AbstractMessageEncoder;
+import org.opensaml.profile.action.ActionSupport;
+import org.opensaml.profile.action.EventIds;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.slf4j.Logger;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.nimbusds.jwt.JWT;
+import com.nimbusds.jwt.SignedJWT;
+import com.nimbusds.oauth2.sdk.Request;
+
+import jakarta.servlet.http.HttpServletRequest;
+import net.shibboleth.idp.plugin.oidc.op.encoding.impl.ResponseUtil;
+import net.shibboleth.idp.plugin.oidc.op.oauth2.messaging.context.OAuth2DPoPProofContext;
+import net.shibboleth.idp.plugin.oidc.op.profile.impl.AbstractOIDCRequestAction;
+import net.shibboleth.idp.profile.IdPEventIds;
+import net.shibboleth.oidc.profile.core.OidcEventIds;
+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;
+
+/**
+ * Action that stores DPoP proof JWT to {@link OAuth2DPoPProofContext}. The value is extracted from the HTTP request
+ * header. If the DPoP-header contains a value, this action verifies that it's a single SignedJWT value as mandated by
+ * the specification.
+ *
+ * @since 4.2.0
+ */
+public class InitializeDPoPProofContext extends AbstractOIDCRequestAction<Request> {
+
+ /** Class logger. */
+ @Nonnull private Logger log = LoggerFactory.getLogger(InitializeDPoPProofContext.class);
+
+ /** Used to log protocol messages. */
+ @Nonnull private Logger protocolMessageLog =
+ LoggerFactory.getLogger(AbstractMessageEncoder.BASE_PROTOCOL_MESSAGE_LOGGER_CATEGORY + ".OAUTH2");
+
+ /** Predicate for enforcing the use of DPoP proofs. */
+ @NonnullAfterInit private Predicate<ProfileRequestContext> requireDpopProofCondition;
+
+ /** Object mapper used for pretty-printing JWT contents. */
+ @NonnullAfterInit private ObjectMapper objectMapper;
+
+ /**
+ * Set the predicate for enforcing the use of DPoP proofs.
+ *
+ * @param predicate the predicate for enforcing the use of DPoP proofs
+ */
+ public void setRequireDpopProofCondition(@Nonnull final Predicate<ProfileRequestContext> predicate) {
+ requireDpopProofCondition = Constraint.isNotNull(predicate,
+ "DPoP proof enforced predicate annot be null");
+ }
+
+ /**
+ * Set the object mapper used for pretty-printing JWT contents.
+ *
+ * @param mapper What to set.
+ */
+ public void setObjectMapper(@Nonnull final ObjectMapper mapper) {
+ checkSetterPreconditions();
+ objectMapper = Constraint.isNotNull(mapper, "Object mapper cannot be null");
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected void doInitialize() throws ComponentInitializationException {
+ super.doInitialize();
+ if (requireDpopProofCondition == null) {
+ throw new ComponentInitializationException("DPoP proof enforced predicate cannot be null");
+ }
+ if (objectMapper == null) {
+ throw new ComponentInitializationException("Object mapper cannot be null");
+ }
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected boolean doPreExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
+ if (!super.doPreExecute(profileRequestContext)) {
+ return false;
+ }
+
+ final HttpServletRequest httpServletRequest = getHttpServletRequest();
+ if (httpServletRequest == null) {
+ log.error("{} No HttpServletRequest available", getLogPrefix());
+ ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
+ return false;
+ }
+
+ final boolean valueExists = httpServletRequest.getHeader("DPoP") != null;
+ final boolean requireDpopProof = requireDpopProofCondition.test(profileRequestContext);
+ if (!valueExists) {
+ if (!requireDpopProof) {
+ log.debug("{} No optional DPoP Proof header values exists, nothing to do", getLogPrefix());
+ return false;
+ }
+ log.warn("{} No mandatory DPoP Proof header value exists", getLogPrefix());
+ ActionSupport.buildEvent(profileRequestContext, OidcEventIds.MISSING_DPOP_PROOF);
+ return false;
+ }
+
+ return true;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
+ final HttpServletRequest httpServletRequest = getHttpServletRequest();
+ assert httpServletRequest != null;
+
+ final String dpopProofValue = getSingleValueOrNull(httpServletRequest.getHeaders("DPoP"));
+ if (dpopProofValue == null) {
+ ActionSupport.buildEvent(profileRequestContext, OidcEventIds.MISSING_DPOP_PROOF);
+ return;
+ }
+ final OAuth2DPoPProofContext proofContext = profileRequestContext
+ .ensureInboundMessageContext().ensureSubcontext(OAuth2DPoPProofContext.class);
+ final SignedJWT jwt;
+ try {
+ jwt = SignedJWT.parse(dpopProofValue);
+ if (jwt != null) {
+ proofContext.setDpopProof(jwt);
+ log.debug("{} DPoP Proof value {} stored into the response context", getLogPrefix(), dpopProofValue);
+ doProtocolLog(profileRequestContext, jwt, "DPoP proof JWT payload contents");
+ return;
+ } else {
+ log.warn("{} DPoP Proof value {} parsing returned a null JWT", getLogPrefix(), dpopProofValue);
+ }
+ } catch (ParseException e) {
+ log.warn("{} Could not parse the DPoP Proof value {} into a signed JWT", getLogPrefix(), dpopProofValue, e);
+ }
+ ActionSupport.buildEvent(profileRequestContext, OidcEventIds.INVALID_DPOP_PROOF);
+ }
+
+ /**
+ * Returns the value from given enumeration if only single value exists, null otherwise.
+ * @param enumeration the input enumeration
+ * @return the single value if exists, null otherwise
+ */
+ @Nullable protected String getSingleValueOrNull(@Nullable final Enumeration<String> enumeration) {
+ if (enumeration == null) {
+ log.warn("{} No mandatory DPoP Proof header value exists", getLogPrefix());
+ return null;
+ }
+ final List<String> list = Collections.list(enumeration);
+ assert list != null;
+ if (list.isEmpty()) {
+ log.warn("{} No mandatory DPoP Proof header value exists", getLogPrefix());
+ return null;
+ }
+ if (list.size() > 1) {
+ log.warn("{} More than one DPoP Proof header value exists, not allowed", getLogPrefix());
+ return null;
+ }
+ return list.get(0);
+ }
+
+ /**
+ * Create a protocol message containing the pretty-printed contents of the given JWT.
+ *
+ * @param profileRequestContext Profile request context where to publish possible error event
+ * @param jwt The token to be logged
+ * @param messagePrefix The prefix for the protocol log message
+ */
+ protected void doProtocolLog(@Nonnull final ProfileRequestContext profileRequestContext,
+ @Nonnull final JWT jwt, @Nonnull final String messagePrefix) {
+ try {
+ assert objectMapper != null;
+ protocolMessageLog.trace("{}:\n{}", messagePrefix, ResponseUtil.getJwtProtocolMessage(jwt, objectMapper));
+ } catch (ParseException e) {
+ log.error("{} Could not produce the protocol logger message", getLogPrefix(), e);
+ ActionSupport.buildEvent(profileRequestContext, IdPEventIds.INVALID_PROFILE_CONFIG);
+ }
+ }
+
+}
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/SetAuthorizationCodeToResponseContext.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/SetAuthorizationCodeToResponseContext.java
index bad17758..c95047c1 100644
--- a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/SetAuthorizationCodeToResponseContext.java
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/SetAuthorizationCodeToResponseContext.java
@@ -407,6 +407,7 @@ public class SetAuthorizationCodeToResponseContext extends AbstractOAuthAuthoriz
.setConsentedClaims(consented)
.setConsentEnabled(consentEnabledPredicate.test(profileRequestContext))
.setSessionIdentifier(responseCtx.getSessionId())
+ .setDpopProofJwkThumbprint(responseCtx.getDpopProofJwkThumbprint())
.build();
if (manipulationStrategy != null) {
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/StoreDPoPProofKeyThumbprint.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/StoreDPoPProofKeyThumbprint.java
new file mode 100644
index 00000000..2671257a
--- /dev/null
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/StoreDPoPProofKeyThumbprint.java
@@ -0,0 +1,142 @@
+/*
+ * 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.idp.plugin.oidc.op.oauth2.profile.impl;
+
+import java.util.function.Function;
+import java.util.function.Predicate;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.profile.action.ActionSupport;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.slf4j.Logger;
+
+import net.shibboleth.idp.plugin.oidc.op.messaging.context.OIDCAuthenticationResponseContext;
+import net.shibboleth.idp.plugin.oidc.op.profile.context.navigate.DefaultDPoPProofThumbprintLookupFunction;
+import net.shibboleth.idp.plugin.oidc.op.profile.context.navigate.DefaultRequestDPoPJktLookupFunction;
+import net.shibboleth.oidc.profile.config.logic.RequireDPoPJktParameterPredicate;
+import net.shibboleth.oidc.profile.core.OidcEventIds;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.primitive.LoggerFactory;
+import net.shibboleth.shared.primitive.StringSupport;
+
+/**
+ * Stores the dpop_jkt parameter from the incoming authorization request. It's optional by the spec, but it may be
+ * required via profile configuration settings.
+ */
+public class StoreDPoPProofKeyThumbprint extends AbstractOAuthAuthorizationResponseAction {
+
+ /** Class logger. */
+ @Nonnull private Logger log = LoggerFactory.getLogger(StoreDPoPProofKeyThumbprint.class);
+
+ /** Strategy used to determine whether to require dpop_jkt parameter. */
+ @Nonnull private Predicate<ProfileRequestContext> requireDpopJktCondition;
+
+ /** Strategy used to locate the dpop_jkt value. */
+ @Nonnull private Function<ProfileRequestContext, String> jktLookupStrategy;
+
+ /** Strategy used to locate thumbprint of validated DPoP Proof JWT. */
+ @Nonnull private Function<ProfileRequestContext, String> dpopProofThumbprintLookupStrategy;
+
+ /** Whether dpop_jkt parameter is mandatory. */
+ private boolean requireJkt;
+
+ /** The JWK thumbprint. */
+ @Nullable private String jwkThumbprint;
+
+ /**
+ * Constructor.
+ */
+ public StoreDPoPProofKeyThumbprint() {
+ requireDpopJktCondition = new RequireDPoPJktParameterPredicate();
+ jktLookupStrategy = new DefaultRequestDPoPJktLookupFunction();
+ dpopProofThumbprintLookupStrategy = new DefaultDPoPProofThumbprintLookupFunction();
+ }
+
+ /**
+ * Set the condition used to determine whether to require dpop_jkt parameter.
+ *
+ * @param condition condition to apply
+ */
+ public void setRequireDpopJktCondition(@Nonnull final Predicate<ProfileRequestContext> condition) {
+ ifInitializedThrowUnmodifiabledComponentException();
+
+ requireDpopJktCondition = Constraint.isNotNull(condition, "Condition cannot be null");
+ }
+
+ /**
+ * Set the strategy used to locate the dpop_jkt value.
+ *
+ * @param strategy lookup strategy
+ */
+ public void setDpopJktLookupStrategy(@Nonnull final Function<ProfileRequestContext, String> strategy) {
+ ifInitializedThrowUnmodifiabledComponentException();
+ jktLookupStrategy = Constraint.isNotNull(strategy, "Lookup strategy cannot be null");
+ }
+
+ /**
+ * Set the strategy used to locate the thumbprint of validated DPoP Proof JWT.
+ *
+ * @param strategy lookup strategy
+ */
+ public void setDpopProofThumbprintLookupStrategy(@Nonnull final Function<ProfileRequestContext, String> strategy) {
+ ifInitializedThrowUnmodifiabledComponentException();
+ dpopProofThumbprintLookupStrategy = Constraint.isNotNull(strategy, "Lookup strategy cannot be null");
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected boolean doPreExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
+ if (!super.doPreExecute(profileRequestContext)) {
+ return false;
+ }
+
+ requireJkt = requireDpopJktCondition.test(profileRequestContext);
+
+ jwkThumbprint = jktLookupStrategy.apply(profileRequestContext);
+
+ if (StringSupport.trimOrNull(jwkThumbprint) == null && !requireJkt) {
+ log.debug("{} No optional dpop_jkt in request, nothing to do", getLogPrefix());
+ return false;
+ }
+
+ return true;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
+ if (StringSupport.trimOrNull(jwkThumbprint) == null) {
+ log.warn("{} No dpop_jkt in authorization request even though required by the profile configuration",
+ getLogPrefix());
+ ActionSupport.buildEvent(profileRequestContext, OidcEventIds.MISSING_DPOP_JKT);
+ return;
+ }
+ final String proofThumbprint = dpopProofThumbprintLookupStrategy.apply(profileRequestContext);
+ assert jwkThumbprint != null;
+ if (proofThumbprint != null && !jwkThumbprint.equals(proofThumbprint)) {
+ log.warn("{} dpop_jkt in authorization request does not match with the validated thumbprint",
+ getLogPrefix());
+ ActionSupport.buildEvent(profileRequestContext, OidcEventIds.INVALID_DPOP_PROOF);
+ return;
+
+ }
+ final OIDCAuthenticationResponseContext oidcResponseContext = getOidcResponseContext();
+ assert oidcResponseContext != null;
+ oidcResponseContext.setDpopProofJwkThumbprint(jwkThumbprint);
+ log.debug("{} JWK thumbprint successfully stored into the context", getLogPrefix());
+ }
+}
\ No newline at end of file
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/ValidateDPoPProof.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/ValidateDPoPProof.java
new file mode 100644
index 00000000..6475e947
--- /dev/null
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/oauth2/profile/impl/ValidateDPoPProof.java
@@ -0,0 +1,158 @@
+/*
+ * 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.idp.plugin.oidc.op.oauth2.profile.impl;
+
+import java.text.ParseException;
+import java.util.function.Function;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.profile.action.ActionSupport;
+import org.opensaml.profile.action.EventIds;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.slf4j.Logger;
+
+import com.nimbusds.jose.JOSEException;
+import com.nimbusds.jose.JOSEObjectType;
+import com.nimbusds.jose.JWSHeader;
+import com.nimbusds.jose.jwk.JWK;
+import com.nimbusds.jwt.JWTClaimsSet;
+import com.nimbusds.jwt.SignedJWT;
+import com.nimbusds.oauth2.sdk.Request;
+
+import net.shibboleth.idp.plugin.oidc.op.oauth2.messaging.context.OAuth2DPoPProofContext;
+import net.shibboleth.idp.plugin.oidc.op.profile.impl.AbstractOIDCRequestAction;
+import net.shibboleth.oidc.jwt.claims.ClaimsValidator;
+import net.shibboleth.oidc.jwt.claims.JWTValidationException;
+import net.shibboleth.oidc.profile.config.navigate.DPoPProofClaimsValidatorLookupFunction;
+import net.shibboleth.oidc.profile.core.OidcEventIds;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.primitive.LoggerFactory;
+
+/**
+ * Action validates DPoP Proof JWT if already stored to the context. If validation succeeds, the JWK thumbprint is
+ * stored in the context.
+ */
+public class ValidateDPoPProof extends AbstractOIDCRequestAction<Request> {
+
+ /** Class logger. */
+ @Nonnull private Logger log = LoggerFactory.getLogger(ValidateDPoPProof.class);
+
+ /** Lookup for the claims validator to be applied for validating the DPoP proof. */
+ @Nonnull private Function<ProfileRequestContext, ClaimsValidator> claimsValidatorLookupStrategy;
+
+ /** The claims validator to be applied for validating the DPoP proof. */
+ @Nullable private ClaimsValidator claimsValidator;
+
+ /**
+ * Constructor.
+ */
+ public ValidateDPoPProof() {
+ claimsValidatorLookupStrategy = new DPoPProofClaimsValidatorLookupFunction();
+ }
+ /**
+ * Set the lookup strategy for the claims validator used for validating the DPoP proof.
+ *
+ * @param strategy What to set
+ */
+ public void setClaimsValidatorLookupStrategy(
+ @Nonnull final Function<ProfileRequestContext, ClaimsValidator> strategy) {
+ ifInitializedThrowUnmodifiabledComponentException();
+ claimsValidatorLookupStrategy = Constraint.isNotNull(strategy, "Lookup strategy cannot be null");
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected boolean doPreExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
+ if (!super.doPreExecute(profileRequestContext)) {
+ return false;
+ }
+
+ final OAuth2DPoPProofContext proofContext = profileRequestContext.ensureInboundMessageContext()
+ .getSubcontext(OAuth2DPoPProofContext.class);
+
+ if (proofContext == null || proofContext.getDpopProof() == null) {
+ log.debug("{} No DPoP Proof found from the response context, nothing to do", getLogPrefix());
+ return false;
+ }
+
+ claimsValidator = claimsValidatorLookupStrategy.apply(profileRequestContext);
+ if (claimsValidator == null) {
+ log.error("{} Could not resolve claims validator via lookup", getLogPrefix());
+ ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
+ return false;
+ }
+
+ return true;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
+ final OAuth2DPoPProofContext proofContext = profileRequestContext.ensureInboundMessageContext()
+ .ensureSubcontext(OAuth2DPoPProofContext.class);
+ final SignedJWT dpopProof = proofContext.getDpopProof();
+ assert dpopProof != null;
+ final JWTClaimsSet jwtClaimsSet;
+ try {
+ jwtClaimsSet = dpopProof.getJWTClaimsSet();
+ } catch (ParseException e) {
+ log.warn("{} Could not parse claims set of DPoP Proof", getLogPrefix(), e);
+ ActionSupport.buildEvent(profileRequestContext, OidcEventIds.INVALID_DPOP_PROOF);
+ return;
+ }
+
+ final JWSHeader header = dpopProof.getHeader();
+ if (!new JOSEObjectType("dpop+jwt").equals(header.getType())) {
+ log.warn("{} Unexpected 'typ' parameter value {}", getLogPrefix(), header.getType());
+ ActionSupport.buildEvent(profileRequestContext, OidcEventIds.INVALID_DPOP_PROOF);
+ return;
+ }
+
+ final JWK jwk = header.getJWK();
+ if (jwk == null) {
+ log.warn("{} No value found from 'jwk' parameter value {}", getLogPrefix(), header.getType());
+ ActionSupport.buildEvent(profileRequestContext, OidcEventIds.INVALID_DPOP_PROOF);
+ return;
+ }
+ if (jwk.isPrivate()) {
+ log.warn("{} Private key exists in 'jwk' parameter value {}", getLogPrefix(), header.getType());
+ ActionSupport.buildEvent(profileRequestContext, OidcEventIds.INVALID_DPOP_PROOF);
+ return;
+ }
+
+ assert jwtClaimsSet != null;
+ try {
+ assert claimsValidator != null;
+ claimsValidator.validate(jwtClaimsSet, profileRequestContext);
+ } catch (final JWTValidationException e) {
+ log.warn("{} DPoP Proof JWT validation failed: {}", getLogPrefix(), e.getMessage());
+ ActionSupport.buildEvent(profileRequestContext, OidcEventIds.INVALID_DPOP_PROOF);
+ return;
+ }
+
+ final String jwkThumbprint;
+ try {
+ jwkThumbprint = jwk.computeThumbprint().toString();
+ } catch (JOSEException e) {
+ log.error("{} Could not calculate thumbprint for the JWK", getLogPrefix(), e);
+ ActionSupport.buildEvent(profileRequestContext, OidcEventIds.INVALID_DPOP_PROOF);
+ return;
+ }
+ proofContext.setValidatedDpopProofThumbprint(jwkThumbprint);
+ log.trace("{} JWK thumbprint stored in the context", getLogPrefix());
+ }
+}
\ No newline at end of file
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/SetRefreshTokenToResponseContext.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/SetRefreshTokenToResponseContext.java
index a571a20d..1e4729d6 100644
--- a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/SetRefreshTokenToResponseContext.java
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/SetRefreshTokenToResponseContext.java
@@ -345,6 +345,7 @@ public class SetRefreshTokenToResponseContext extends AbstractOIDCResponseAction
assert idGenerator != null;
builder.setJWTID(idGenerator, xmlSafeIdentifier);
builder.setRootTokenIdentifier(rootTokenId);
+ builder.setDpopProofJwkThumbprint(oidcResponseContext.getDpopProofJwkThumbprint());
final RefreshTokenClaimsSet claimsSet = builder.build();
if (manipulationStrategy != null) {
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/ValidateGrant.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/ValidateGrant.java
index 9bebc8ec..2a565ccf 100644
--- a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/ValidateGrant.java
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/ValidateGrant.java
@@ -379,6 +379,7 @@ public class ValidateGrant extends AbstractOIDCTokenResponseAction {
final OIDCAuthenticationResponseContext oidcResponseContext = getOidcResponseContext();
assert oidcResponseContext != null;
oidcResponseContext.setAuthorizationGrantClaimsSet(tokenClaimsSet);
+ oidcResponseContext.setDpopProofJwkThumbprint(tokenClaimsSet.getDpopProofJwkThumbprint());
}
// Checkstyle: CyclomaticComplexity|MethodLength|ReturnCount ON
diff --git a/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oauth2/dpop-proof-validation/dpop-proof-validation-beans.xml b/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oauth2/dpop-proof-validation/dpop-proof-validation-beans.xml
new file mode 100644
index 00000000..ec8b49dc
--- /dev/null
+++ b/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oauth2/dpop-proof-validation/dpop-proof-validation-beans.xml
@@ -0,0 +1,87 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<beans xmlns="http://www.springframework.org/schema/beans" xmlns:c="http://www.springframework.org/schema/c"
+ xmlns:context="http://www.springframework.org/schema/context" xmlns:p="http://www.springframework.org/schema/p"
+ xmlns:util="http://www.springframework.org/schema/util" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
+ 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">
+
+ <bean id="InitializeDPoPProofContext" class="net.shibboleth.idp.plugin.oidc.op.oauth2.profile.impl.InitializeDPoPProofContext"
+ scope="prototype"
+ p:httpServletRequestSupplier-ref="shibboleth.HttpServletRequestSupplier"
+ p:objectMapper-ref="#{'%{idp.oidc.logging.objectMapper:shibboleth.oidc.JSONObjectMapper}'.trim()}">
+ <property name="requireDpopProofCondition">
+ <bean class="net.shibboleth.oidc.profile.config.logic.RequireDPoPProofPredicate"/>
+ </property>
+ </bean>
+
+ <bean id="DPoPSecurityParametersContextProfileRequestContextLookup" parent="shibboleth.Functions.Compose">
+ <constructor-arg name="g">
+ <ref bean="DPoPSecurityParametersContextMessageContextLookup" />
+ </constructor-arg>
+ <constructor-arg name="f">
+ <ref bean="shibboleth.MessageContextLookup.Inbound" />
+ </constructor-arg>
+ </bean>
+
+ <bean id="DPoPSecurityParametersContextMessageContextLookup" parent="shibboleth.Functions.Compose">
+ <constructor-arg name="g">
+ <bean class="org.opensaml.messaging.context.navigate.ChildContextLookup"
+ c:type="#{ T(net.shibboleth.oidc.security.jose.context.SecurityParametersContext) }"
+ c:createContext="true" />
+ </constructor-arg>
+ <constructor-arg name="f">
+ <bean class="org.opensaml.messaging.context.navigate.ChildContextLookup"
+ c:type="#{ T(net.shibboleth.idp.plugin.oidc.op.oauth2.messaging.context.OAuth2DPoPProofContext) }"/>
+ </constructor-arg>
+ </bean>
+
+ <bean id="PopulateDPoPProofSignatureValidationParameters"
+ class="net.shibboleth.oidc.profile.impl.PopulateJWTSignatureValidationParameters"
+ scope="prototype"
+ c:strategy-ref="shibboleth.MessageContextLookup.Inbound"
+ p:securityParametersContextLookupStrategy-ref="DPoPSecurityParametersContextProfileRequestContextLookup">
+ <property name="configurationLookupStrategy">
+ <bean class="net.shibboleth.oidc.profile.config.navigate.DPoPProofSignatureValidationConfigurationLookupFunction" />
+ </property>
+ <property name="signatureValidationParametersResolver">
+ <bean class="net.shibboleth.oidc.security.jose.impl.BasicSignatureValidationParametersResolver" />
+ </property>
+ <property name="activationCondition">
+ <bean parent="shibboleth.Conditions.Expression"
+ c:expression="#input.ensureInboundMessageContext().ensureSubcontext(T(net.shibboleth.idp.plugin.oidc.op.oauth2.messaging.context.OAuth2DPoPProofContext)).getDpopProof() != null" />
+ </property>
+ </bean>
+
+ <bean id="ValidateDPoPProofSignature" class="net.shibboleth.idp.profile.impl.WebFlowMessageHandlerAdaptor"
+ scope="prototype" c:executionDirection="INBOUND">
+ <constructor-arg>
+ <bean class="org.opensaml.messaging.handler.impl.BasicMessageHandlerChain">
+ <property name="handlers">
+ <list>
+ <bean class="net.shibboleth.oidc.security.impl.JWTMessageSignatureSecurityHandler"
+ scope="prototype"
+ p:securityParametersContextLookupStrategy-ref="DPoPSecurityParametersContextMessageContextLookup">
+ <property name="jwtTokenLookupStrategy">
+ <bean
+ class="net.shibboleth.profile.context.navigate.SpringExpressionContextLookupFunction"
+ c:_0="#{ T(org.opensaml.messaging.context.MessageContext) }"
+ c:outputType="#{T(com.nimbusds.jwt.SignedJWT)}"
+ c:expression="#input.getParent().ensureInboundMessageContext().ensureSubcontext(T(net.shibboleth.idp.plugin.oidc.op.oauth2.messaging.context.OAuth2DPoPProofContext)).getDpopProof()" />
+ </property>
+ </bean>
+ </list>
+ </property>
+ </bean>
+ </constructor-arg>
+ <property name="activationCondition">
+ <bean parent="shibboleth.Conditions.Expression"
+ c:expression="#input.ensureInboundMessageContext().ensureSubcontext(T(net.shibboleth.idp.plugin.oidc.op.oauth2.messaging.context.OAuth2DPoPProofContext)).getDpopProof() != null" />
+ </property>
+ </bean>
+
+ <bean id="ValidateDPoPProof" class="net.shibboleth.idp.plugin.oidc.op.oauth2.profile.impl.ValidateDPoPProof"
+ scope="prototype" />
+
+</beans>
diff --git a/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oauth2/dpop-proof-validation/dpop-proof-validation-flow.xml b/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oauth2/dpop-proof-validation/dpop-proof-validation-flow.xml
new file mode 100644
index 00000000..a4db6639
--- /dev/null
+++ b/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oauth2/dpop-proof-validation/dpop-proof-validation-flow.xml
@@ -0,0 +1,17 @@
+<flow xmlns="http://www.springframework.org/schema/webflow"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://www.springframework.org/schema/webflow http://www.springframework.org/schema/webflow/spring-webflow.xsd"
+ abstract="true">
+
+ <action-state id="DoDPoPProofValidation">
+ <evaluate expression="InitializeDPoPProofContext" />
+ <evaluate expression="PopulateDPoPProofSignatureValidationParameters" />
+ <evaluate expression="ValidateDPoPProofSignature" />
+ <evaluate expression="ValidateDPoPProof" />
+ <evaluate expression="'proceed'" />
+ <transition on="proceed" to="DoAuthenticationSubflow" />
+ </action-state>
+
+ <bean-import resource="classpath:/META-INF/net/shibboleth/idp/flows/oauth2/dpop-proof-validation/dpop-proof-validation-beans.xml" />
+
+</flow>
diff --git a/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oauth2/pushed-authorization/pushed-authorization-beans.xml b/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oauth2/pushed-authorization/pushed-authorization-beans.xml
index 31667fd0..37311a58 100644
--- a/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oauth2/pushed-authorization/pushed-authorization-beans.xml
+++ b/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oauth2/pushed-authorization/pushed-authorization-beans.xml
@@ -289,6 +289,9 @@
<bean id="ValidateCodeChallenge" class="net.shibboleth.idp.plugin.oidc.op.oauth2.profile.impl.ValidateCodeChallenge"
scope="prototype" />
+ <bean id="StoreDPoPProofKeyThumbprint" class="net.shibboleth.idp.plugin.oidc.op.oauth2.profile.impl.StoreDPoPProofKeyThumbprint"
+ scope="prototype" />
+
<bean id="FormOutboundMessage"
class="net.shibboleth.idp.plugin.oidc.op.oauth2.profile.impl.FormOutbounPushedAuthorizationResponseMessage"
scope="prototype"
diff --git a/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oauth2/pushed-authorization/pushed-authorization-flow.xml b/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oauth2/pushed-authorization/pushed-authorization-flow.xml
index 907cf8be..04327da2 100644
--- a/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oauth2/pushed-authorization/pushed-authorization-flow.xml
+++ b/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oauth2/pushed-authorization/pushed-authorization-flow.xml
@@ -50,6 +50,7 @@
<evaluate expression="ValidateResponseType" />
<evaluate expression="ValidateResponseMode" />
<evaluate expression="ValidateCodeChallenge" />
+ <evaluate expression="StoreDPoPProofKeyThumbprint" />
<evaluate expression="'proceed'" />
<transition on="proceed" to="PopulateOutboundInterceptContext" />
diff --git a/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/abstract-api/oidc-abstract-api-flow.xml b/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/abstract-api/oidc-abstract-api-flow.xml
index 6900d8b1..4d733b29 100644
--- a/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/abstract-api/oidc-abstract-api-flow.xml
+++ b/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/abstract-api/oidc-abstract-api-flow.xml
@@ -1,7 +1,7 @@
<flow xmlns="http://www.springframework.org/schema/webflow"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/webflow http://www.springframework.org/schema/webflow/spring-webflow.xsd"
- parent="oidc/abstract"
+ parent="oidc/abstract, oauth2/dpop-proof-validation"
abstract="true">
<!-- Actions common to most OIDC profile backend flows. -->
@@ -20,7 +20,7 @@
<evaluate expression="InitializeAuthenticationContext" />
<evaluate expression="'proceed'" />
- <transition on="proceed" to="DoAuthenticationSubflow" />
+ <transition on="proceed" to="DoDPoPProofValidation" />
</action-state>
<subflow-state id="DoAuthenticationSubflow" subflow="authn">
diff --git a/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/authorize/authorize-beans.xml b/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/authorize/authorize-beans.xml
index 5796da38..a6a14945 100644
--- a/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/authorize/authorize-beans.xml
+++ b/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/authorize/authorize-beans.xml
@@ -356,6 +356,9 @@
<bean id="ValidateCodeChallenge" class="net.shibboleth.idp.plugin.oidc.op.oauth2.profile.impl.ValidateCodeChallenge"
scope="prototype" />
+ <bean id="StoreDPoPProofKeyThumbprint" class="net.shibboleth.idp.plugin.oidc.op.oauth2.profile.impl.StoreDPoPProofKeyThumbprint"
+ scope="prototype" />
+
<bean id="ValidateScope" class="net.shibboleth.idp.plugin.oidc.op.oauth2.profile.impl.ValidateScope" scope="prototype"
p:allowedScopeLookupStrategy="#{getObject('shibboleth.oidc.AllowedScopeStrategy') ?: getObject('shibboleth.oidc.DefaultAllowedScopeStrategy')}" />
diff --git a/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/authorize/authorize-flow.xml b/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/authorize/authorize-flow.xml
index 7dab858e..954deb12 100644
--- a/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/authorize/authorize-flow.xml
+++ b/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/authorize/authorize-flow.xml
@@ -68,6 +68,7 @@
<evaluate expression="ValidateResponseType" />
<evaluate expression="ValidateResponseMode" />
<evaluate expression="ValidateCodeChallenge" />
+ <evaluate expression="StoreDPoPProofKeyThumbprint" />
<evaluate expression="SetRequestedClaimsToResponseContext" />
<evaluate expression="SetRequestedSubjectToResponseContext" />
<evaluate expression="SetSessionIdToResponseContext" />
diff --git a/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/service/relying-party/postconfig.xml b/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/service/relying-party/postconfig.xml
index 13a2aac4..4dca5e0c 100644
--- a/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/service/relying-party/postconfig.xml
+++ b/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/service/relying-party/postconfig.xml
@@ -88,7 +88,9 @@
p:issuer-ref="shibboleth.oidc.issuer"
p:tokenEndpointAuthMethods="%{idp.oidc.par.tokenEndpointAuthMethods:client_secret_basic,client_secret_post,client_secret_jwt,private_key_jwt}"
p:claimsValidator-ref="DefaultJWTClaimsValidator"
- p:unregisteredClientPolicy="#{getObject('shibboleth.oidc.DefaultUnregisteredClientPolicy')}" />
+ p:dpopProofClaimsValidator-ref="DefaultDPoPProofClaimsValidator"
+ p:unregisteredClientPolicy="#{getObject('shibboleth.oidc.DefaultUnregisteredClientPolicy')}"
+ p:dpopProofSignatureValidationConfiguration-ref="DPoPSignatureValidationConfiguration" />
<bean id="DefaultLogoutHintMatchingPredicate"
class="net.shibboleth.idp.plugin.oidc.op.profile.logic.DefaultLogoutHintMatchingPredicate"/>
@@ -256,6 +258,16 @@
p:propertyType="#{T(java.util.function.Function)}"
p:defaultValue-ref="shibboleth.oidc.DefaultUnregisteredClientPolicy" />
</property>
+ <property name="dpopProofClaimsValidatorLookupStrategy">
+ <bean parent="shibboleth.MDDrivenBeanProperty" p:propertyName="dpopProofClaimsValidator"
+ p:propertyType="#{T(net.shibboleth.oidc.jwt.claims.ClaimsValidator)}"
+ p:defaultValue-ref="DefaultDPoPProofClaimsValidator" />
+ </property>
+ <property name="dpopProofSignatureValidationConfigurationLookupStrategy">
+ <bean parent="shibboleth.MDDrivenBeanProperty" p:propertyName="dpopProofSignatureValidationConfiguration"
+ p:propertyType="#{T(net.shibboleth.oidc.security.jose.SignatureValidationConfiguration)}"
+ p:defaultValue-ref="DPoPSignatureValidationConfiguration" />
+ </property>
</bean>
<bean id="OIDC.SSO.MDDriven" parent="AbstractMDDrivenOIDCSSOProfile" lazy-init="true"
@@ -661,6 +673,16 @@
p:propertyType="#{T(java.util.function.Function)}"
p:defaultValue-ref="shibboleth.oidc.DefaultUnregisteredClientPolicy" />
</property>
+ <property name="dpopProofClaimsValidatorLookupStrategy">
+ <bean parent="shibboleth.MDDrivenBeanProperty" p:propertyName="dpopProofClaimsValidator"
+ p:propertyType="#{T(net.shibboleth.oidc.jwt.claims.ClaimsValidator)}"
+ p:defaultValue-ref="DefaultDPoPProofClaimsValidator" />
+ </property>
+ <property name="dpopProofSignatureValidationConfigurationLookupStrategy">
+ <bean parent="shibboleth.MDDrivenBeanProperty" p:propertyName="dpopProofSignatureValidationConfiguration"
+ p:propertyType="#{T(net.shibboleth.oidc.security.jose.SignatureValidationConfiguration)}"
+ p:defaultValue-ref="DPoPSignatureValidationConfiguration" />
+ </property>
</bean>
<!-- Default client-auth JWT validation wiring. -->
@@ -954,4 +976,38 @@
</constructor-arg>
</bean>
+ <bean id="DPoPSignatureValidationConfiguration"
+ parent="shibboleth.oidc.BasicSignatureValidationConfiguration"
+ p:signatureTrustEngine-ref="TokenAsymmetricKeyTrustEngineForDPoPJWT"/>
+
+ <bean id="TokenAsymmetricKeyTrustEngineForDPoPJWT"
+ class="net.shibboleth.oidc.security.impl.TokenAsymmetricKeyTrustEngine"
+ c:JOSEObjectResolver-ref="defaultSignedJWTJOSEHeaderCredentialResolver" />
+
+ <bean id="DefaultDPoPProofClaimsValidator"
+ class="net.shibboleth.oidc.security.jwt.claims.impl.ChainingJWTClaimsValidator"
+ p:claimValidators-ref="DPoPProofClaimsValidators" />
+
+ <util:list id="DPoPProofClaimsValidators" value-type="net.shibboleth.oidc.jwt.claims.ClaimsValidator">
+ <ref bean="ExpiryClaimsValidator" />
+ <ref bean="NotBeforeClaimsValidator" />
+ <ref bean="IssuedAtClaimsValidator" />
+ <bean id="JWTIdentifierClaimsValidator"
+ class="net.shibboleth.oidc.security.jwt.claims.impl.JWTIdentifierClaimsValidator"
+ p:clockSkew="%{idp.policy.clockSkew:PT1M}"
+ p:replayCache-ref="shibboleth.ReplayCache"
+ p:replayCacheRecordLifetime="%{idp.oauth2.dpop.replayCacheLifetime:PT5M}" />
+ <bean class="net.shibboleth.oidc.security.jwt.claims.impl.ExactMatchClaimsValidator" p:claimName="htm">
+ <property name="valueToMatchLookupStrategy">
+ <bean parent="shibboleth.BiFunctions.Constant" c:target="POST" />
+ </property>
+ </bean>
+ <bean class="net.shibboleth.oidc.security.jwt.claims.impl.ExactMatchClaimsValidator" p:claimName="htu">
+ <property name="valueToMatchLookupStrategy">
+ <bean parent="shibboleth.BiFunctions.Expression" c:expression="#custom.get().getRequestURL().toString()"
+ p:customObject-ref="shibboleth.HttpServletRequestSupplier"/>
+ </property>
+ </bean>
+ </util:list>
+
</beans>
diff --git a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/AbstractOidcFlowTest.java b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/AbstractOidcFlowTest.java
index 65d8faa9..ae0d9885 100644
--- a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/AbstractOidcFlowTest.java
+++ b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/AbstractOidcFlowTest.java
@@ -81,6 +81,8 @@ import com.nimbusds.oauth2.sdk.ResponseType;
import com.nimbusds.oauth2.sdk.Scope;
import com.nimbusds.oauth2.sdk.auth.ClientAuthenticationMethod;
import com.nimbusds.oauth2.sdk.auth.Secret;
+import com.nimbusds.oauth2.sdk.dpop.DPoPProofFactory;
+import com.nimbusds.oauth2.sdk.dpop.DefaultDPoPProofFactory;
import com.nimbusds.oauth2.sdk.id.ClientID;
import com.nimbusds.oauth2.sdk.token.BearerAccessToken;
import com.nimbusds.openid.connect.sdk.claims.ClaimsSet;
@@ -601,4 +603,19 @@ public abstract class AbstractOidcFlowTest extends AbstractFlowTest {
return new BearerAccessToken(claims.serialize(getDataSealer()));
}
+ protected static SignedJWT buildDPoPProof(final String method, final String uri) {
+ try {
+ ECKey jwk = new ECKeyGenerator(Curve.P_256)
+ .keyID("1")
+ .generate();
+ DPoPProofFactory proofFactory = new DefaultDPoPProofFactory(
+ jwk,
+ JWSAlgorithm.ES256);
+ return proofFactory.createDPoPJWT(method, new URI(uri));
+ } catch (JOSEException | URISyntaxException e) {
+ Assert.fail("Could not create DPoP proof", e);
+ }
+ return null;
+ }
+
}
diff --git a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/AuthorizeFlowTest.java b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/AuthorizeFlowTest.java
index ea8d57d5..18209036 100644
--- a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/AuthorizeFlowTest.java
+++ b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/AuthorizeFlowTest.java
@@ -1747,6 +1747,50 @@ public class AuthorizeFlowTest extends AbstractOidcFlowTest {
Assert.assertNull(successResponse.getIssuer());
}
+ @Test
+ public void testWithOptionalDPoPJkt_authzRequest() throws IOException, ParseException,
+ SessionException, JOSEException {
+ request.setMethod("GET");
+ setRequestParameters(List.of(new Pair<>("client_id", "mockClientId"),
+ new Pair<>("response_type", "code"),
+ new Pair<>("scope", "profile"),
+ new Pair<>("redirect_uri", redirectUri),
+ new Pair<>("dpop_jkt", "mockDPoPJktValue")));
+ storeMetadata(storageService, clientId, clientSecret, scope, redirectUri);
+
+ initializeThreadLocals();
+
+ final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+ final AuthorizationResponse responseMessage = parseSuccessResponse(result, AuthorizationResponse.class);
+ final AuthorizationSuccessResponse successResponse = responseMessage.toSuccessResponse();
+ Assert.assertEquals(successResponse.getRedirectionURI().toString(), redirectUri);
+ Assert.assertNull(successResponse.getAccessToken());
+ Assert.assertNotNull(successResponse.getAuthorizationCode());
+ Assert.assertEquals(getJktFromAuthorizeCodeClaimsSet(successResponse), "mockDPoPJktValue");
+ }
+
+ @Test
+ public void testWithOptionalDPoPJkt_authnRequest() throws IOException, ParseException,
+ SessionException, JOSEException {
+ request.setMethod("GET");
+ setRequestParameters(List.of(new Pair<>("client_id", "mockClientId"),
+ new Pair<>("response_type", "code"),
+ new Pair<>("scope", "openid profile"),
+ new Pair<>("redirect_uri", redirectUri),
+ new Pair<>("dpop_jkt", "mockDPoPJktValue")));
+ storeMetadata(storageService, clientId, clientSecret, scope, redirectUri);
+
+ initializeThreadLocals();
+
+ final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+ final AuthenticationResponse responseMessage = parseSuccessResponse(result, AuthenticationResponse.class);
+ final AuthenticationSuccessResponse successResponse = responseMessage.toSuccessResponse();
+ Assert.assertEquals(successResponse.getRedirectionURI().toString(), redirectUri);
+ Assert.assertNull(successResponse.getAccessToken());
+ Assert.assertNotNull(successResponse.getAuthorizationCode());
+ Assert.assertEquals(getJktFromAuthorizeCodeClaimsSet(successResponse), "mockDPoPJktValue");
+ }
+
@Factory
public Object[] createIdTokenSecurityTests() {
return new Object[] {
@@ -1945,6 +1989,18 @@ public class AuthorizeFlowTest extends AbstractOidcFlowTest {
}
}
+ protected String getJktFromAuthorizeCodeClaimsSet(final AuthorizationSuccessResponse successResponse) {
+ Assert.assertNotNull(successResponse.getAuthorizationCode());
+ final AuthorizeCodeClaimsSet claims;
+ try {
+ claims = AuthorizeCodeClaimsSet.parse(successResponse.getAuthorizationCode().getValue(), getDataSealer());
+ Assert.assertNotNull(claims.getDpopProofJwkThumbprint());
+ return claims.getDpopProofJwkThumbprint();
+ } catch (ParseException | DataSealerException e) {
+ return null;
+ }
+ }
+
protected List<String> getAudienceFromAuthorizeCodeClaimsSet(final AuthenticationSuccessResponse successResponse) {
Assert.assertNotNull(successResponse.getAuthorizationCode());
final AuthorizeCodeClaimsSet claims;
diff --git a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/PushedAuthorizeFlowTest.java b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/PushedAuthorizeFlowTest.java
index ea3ec55c..82476eb7 100644
--- a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/PushedAuthorizeFlowTest.java
+++ b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/PushedAuthorizeFlowTest.java
@@ -192,6 +192,44 @@ public class PushedAuthorizeFlowTest extends AbstractOidcClientAuthenticationFlo
removeMetadata(storageService, "mockClientIdRequestObjectEnforced");
}
+ @Test
+ public void testWithInvalidSyntaxDPoPProof() throws IOException, SessionException {
+ storeMetadata(storageService, clientId, clientSecret, scope, "https://example.org/cb");
+ setBasicAuth(clientId, clientSecret);
+ request.addHeader("DPoP", "invalid");
+ setHttpFormRequest("POST", createRequestParameters(clientId));
+ final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+ assertErrorCode(result, OAuth2Error.INVALID_REQUEST_CODE);
+ removeMetadata(storageService, "mockClientIdRequestObjectEnforced");
+ }
+
+ @SuppressWarnings("null")
+ @Test
+ public void testWithInvalidDPoPProof_thunbprintNotMatching() throws IOException, SessionException {
+ storeMetadata(storageService, clientId, clientSecret, scope, "https://example.org/cb");
+ setBasicAuth(clientId, clientSecret);
+ request.addHeader("DPoP", buildDPoPProof("POST", "http://localhost/idp/profile/oauth2/pushed-authorization").serialize());
+ Map<String, String> requestParameters = createRequestParameters(clientId);
+ requestParameters.put("dpop_jkt", "notMatching");
+ setHttpFormRequest("POST", requestParameters);
+ final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+ assertErrorCode(result, OAuth2Error.INVALID_REQUEST_CODE);
+ }
+
+ @SuppressWarnings("null")
+ @Test
+ public void testWithValidDPoPProof() throws IOException, SessionException {
+ storeMetadata(storageService, clientId, clientSecret, scope, "https://example.org/cb");
+ setBasicAuth(clientId, clientSecret);
+ request.addHeader("DPoP", buildDPoPProof("POST", "http://localhost/idp/profile/oauth2/pushed-authorization").serialize());
+ setHttpFormRequest("POST", createRequestParameters(clientId));
+ final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+ assertSuccessResponse(result, clientId);
+ final PushedAuthorizationSuccessResponse response =
+ parseSuccessResponse(result, PushedAuthorizationSuccessResponse.class);
+ verifyAuthorizeEndpoint(clientId, response.getRequestURI().toString());
+ }
+
@Factory
public Object[] createRequestObjectSecurityTests() {
return new Object[] {
--
To stop receiving notification emails like this one, please contact
the administrator of this repository.
More information about the commits
mailing list