[java-idp-oidc] branch main updated: JOIDC-201 - Support for OAuth 2.0 Demonstrating Proof of Possession (DPoP)

Henri Mikkonen henri.mikkonen at iki.fi
Wed May 15 14:49:48 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=f7abeaf156ab82d1ae4561f0f386408f74cfaeb8

The following commit(s) were added to refs/heads/main by this push:
     new f7abeaf1 JOIDC-201 - Support for OAuth 2.0 Demonstrating Proof of Possession (DPoP)
f7abeaf1 is described below

commit f7abeaf156ab82d1ae4561f0f386408f74cfaeb8
Author: Henri Mikkonen <henri.mikkonen at iki.fi>
AuthorDate: Wed May 15 17:49:37 2024 +0300

    JOIDC-201 - Support for OAuth 2.0 Demonstrating Proof of Possession (DPoP)
    
    https://shibboleth.atlassian.net/browse/JOIDC-201
    
    - First draft of the DPoP support in UserInfo endpoint
      - If Authorization-header contains DPoP token, proof is also required
      - If thumbprint exists in the claims set, bearer cannot be used
---
 .../logic/DPoPAccessTokenInRequestCondition.java   |  79 ++++++++++++
 .../userinfo/profile/impl/ValidateAccessToken.java |  34 ++++-
 .../dpop-proof-validation-beans.xml                |  10 +-
 .../dpop-proof-validation-flow.xml                 |   7 +-
 .../idp/flows/oidc/userinfo/userinfo-flow.xml      |   9 +-
 .../idp/service/relying-party/postconfig.xml       |  14 +-
 .../op/profile/flow/AbstractOidcApiFlowTest.java   |  25 +++-
 .../AbstractOidcClientAuthenticationFlowTest.java  |   6 -
 .../plugin/oidc/op/profile/flow/UserInfoTest.java  | 143 ++++++++++++++++++++-
 .../DPoPAccessTokenInRequestConditionTest.java     | 103 +++++++++++++++
 10 files changed, 414 insertions(+), 16 deletions(-)

diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/logic/DPoPAccessTokenInRequestCondition.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/logic/DPoPAccessTokenInRequestCondition.java
new file mode 100644
index 00000000..eefe955a
--- /dev/null
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/logic/DPoPAccessTokenInRequestCondition.java
@@ -0,0 +1,79 @@
+/*
+ * 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.logic;
+
+import java.util.Enumeration;
+import java.util.function.Predicate;
+import java.util.function.Supplier;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.profile.context.ProfileRequestContext;
+
+import jakarta.servlet.http.HttpServletRequest;
+import net.shibboleth.shared.annotation.constraint.NonnullAfterInit;
+import net.shibboleth.shared.component.AbstractIdentifiableInitializableComponent;
+import net.shibboleth.shared.component.ComponentInitializationException;
+import net.shibboleth.shared.logic.Constraint;
+
+/**
+ * A predicate returning true it detects a DPoP access token from the {@link HttpServletRequest}.
+ * 
+ * @since 4.2.0
+ */
+public class DPoPAccessTokenInRequestCondition extends AbstractIdentifiableInitializableComponent
+    implements Predicate<ProfileRequestContext> {
+
+    /** Supplier for the {@link HttpServletRequest}. */
+    @NonnullAfterInit private Supplier<HttpServletRequest> httpServletRequestSupplier;
+
+    /**
+     * Set the supplier for the {@link HttpServletRequest}.
+     * 
+     * @param strategy What to set.
+     */
+    public void setHttpServletRequestSupplier(final @Nonnull Supplier<HttpServletRequest> supplier) {
+        httpServletRequestSupplier = Constraint.isNotNull(supplier, "Http servlet request supplier cannot be null");
+    }
+
+    /** {@inheritDoc} */
+    protected void doInitialize() throws ComponentInitializationException {
+        super.doInitialize();
+        if (httpServletRequestSupplier == null) {
+            throw new ComponentInitializationException("Http servlet request supplier cannot be null");
+        }
+    }
+    
+    /** {@inheritDoc} */
+    @Override
+    public boolean test(final @Nullable ProfileRequestContext input) {
+        checkComponentActive();
+        final HttpServletRequest httpServletRequest = httpServletRequestSupplier.get();
+        if (httpServletRequest == null) {
+            return false;
+        }
+        final Enumeration<String> authorizationHeaders = httpServletRequest.getHeaders("Authorization");
+        if (authorizationHeaders == null) {
+            return false;
+        }
+        while (authorizationHeaders.hasMoreElements()) {
+            if (authorizationHeaders.nextElement().startsWith("DPoP ")) {
+                return true;
+            }
+        }
+        return false;
+    }
+}
\ No newline at end of file
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/userinfo/profile/impl/ValidateAccessToken.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/userinfo/profile/impl/ValidateAccessToken.java
index c51c5ed1..a99c3959 100644
--- a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/userinfo/profile/impl/ValidateAccessToken.java
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/userinfo/profile/impl/ValidateAccessToken.java
@@ -27,6 +27,7 @@ import org.slf4j.Logger;
 import com.nimbusds.jwt.JWTClaimsSet;
 
 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.token.support.AccessTokenClaimsSet;
 import net.shibboleth.idp.plugin.oidc.op.token.support.TokenClaimsSet;
 import net.shibboleth.idp.profile.IdPEventIds;
@@ -55,13 +56,17 @@ public class ValidateAccessToken extends AbstractOIDCUserInfoValidationResponseA
     
     /** Lookup strategy for claims validator. */
     @Nonnull private Function<ProfileRequestContext,ClaimsValidator> claimsValidatorLookupStrategy;
-    
+
+    /** Strategy used to locate thumbprint of validated DPoP Proof JWT. */
+    @Nonnull private Function<ProfileRequestContext, String> dpopProofThumbprintLookupStrategy;
+
     /** The claims validator to use. */
     @Nullable private ClaimsValidator claimsValidator;
 
     /** Constructor. */
     public ValidateAccessToken() {
         claimsValidatorLookupStrategy = new IssuedClaimsValidatorLookupFunction();
+        dpopProofThumbprintLookupStrategy = new DefaultDPoPProofThumbprintLookupFunction();
     }
 
     /**
@@ -74,7 +79,19 @@ public class ValidateAccessToken extends AbstractOIDCUserInfoValidationResponseA
         ifInitializedThrowUnmodifiabledComponentException();
         claimsValidatorLookupStrategy = 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
+     * 
+     * @since 4.2.0
+     */
+    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) {
@@ -106,6 +123,19 @@ public class ValidateAccessToken extends AbstractOIDCUserInfoValidationResponseA
             ActionSupport.buildEvent(profileRequestContext, OidcEventIds.INVALID_ACCESS_TOKEN);
             return;
         }
+
+        final String claimsSetThumbprint = tokenClaims.getDpopProofJwkThumbprint();
+        final String proofThumbprint = dpopProofThumbprintLookupStrategy.apply(profileRequestContext);
+        if (claimsSetThumbprint != null || proofThumbprint != null) {
+            if (claimsSetThumbprint == null || proofThumbprint == null ||
+                    !claimsSetThumbprint.equals(proofThumbprint)) {
+                log.warn("{} Invalid DPoP Proof thumbprint issued by client {}, invalid for {}", getLogPrefix(),
+                        tokenClaims.getClientID(), claimsSetThumbprint);
+                ActionSupport.buildEvent(profileRequestContext, OidcEventIds.INVALID_DPOP_PROOF);
+                return;
+            }
+        }
+
         final JWTClaimsSet claimsSet = tokenClaims.getClaimsSet();
         assert claimsSet != null;
         log.debug("{} Validating parsed/decoded claims set: {}", getLogPrefix(), claimsSet.toString());
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
index 9c270b19..80b9dfe6 100644
--- 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
@@ -34,7 +34,15 @@
         </constructor-arg>
     </bean>
 
-    <bean id="DPoPProofRequired" class="net.shibboleth.oidc.profile.config.logic.RequireDPoPProofPredicate"/>
+    <bean id="DPoPProofRequired" parent="shibboleth.Conditions.OR">
+        <constructor-arg>
+            <list>
+                <bean class="net.shibboleth.oidc.profile.config.logic.RequireDPoPProofPredicate"/>
+                <bean class="net.shibboleth.idp.plugin.oidc.op.profile.logic.DPoPAccessTokenInRequestCondition"
+                    p:httpServletRequestSupplier-ref="shibboleth.HttpServletRequestSupplier"/>
+            </list>
+        </constructor-arg>
+    </bean>
 
     <bean id="DPoPProofExists" parent="shibboleth.Conditions.Expression"
         c:expression="#input.getInboundMessageContext() != null and #input.getInboundMessageContext().getSubcontext(T(net.shibboleth.idp.plugin.oidc.op.oauth2.messaging.context.OAuth2DPoPProofContext)) != null and #input.getInboundMessageContext().getSubcontext(T(net.shibboleth.idp.plugin.oidc.op.oauth2.messaging.context.OAuth2DPoPProofContext)).getDpopProof() != null" />
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
index a4db6639..2196b17b 100644
--- 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
@@ -9,8 +9,13 @@
         <evaluate expression="ValidateDPoPProofSignature" />
         <evaluate expression="ValidateDPoPProof" />
         <evaluate expression="'proceed'" />
-        <transition on="proceed" to="DoAuthenticationSubflow" />
+        <transition on="proceed" to="ReturnFromDoDPoPProofValidation" />
     </action-state>
+
+    <decision-state id="ReturnFromDoDPoPProofValidation">
+        <if test="flowScope.skipOAuth2ClientAuth"
+            then="ResumeAfterDoDPoPProofValidation" else="DoAuthenticationSubflow" />
+    </decision-state>
     
     <bean-import resource="classpath:/META-INF/net/shibboleth/idp/flows/oauth2/dpop-proof-validation/dpop-proof-validation-beans.xml" />
 
diff --git a/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/userinfo/userinfo-flow.xml b/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/userinfo/userinfo-flow.xml
index a4d8d709..e180e7b9 100644
--- a/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/userinfo/userinfo-flow.xml
+++ b/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/userinfo/userinfo-flow.xml
@@ -34,16 +34,19 @@
     </action-state>
 
     <decision-state id="CheckInboundInterceptContext">
+        <on-entry>
+            <set name="flowScope.skipOAuth2ClientAuth" value="true" />
+        </on-entry>
         <if test="opensamlProfileRequestContext.ensureSubcontext(T(net.shibboleth.idp.profile.context.ProfileInterceptorContext)).getAvailableFlows().isEmpty()"
-            then="OutboundContextsAndSecurityParameters" else="DoInboundInterceptSubflow" />
+            then="DoDPoPProofValidation" else="DoInboundInterceptSubflow" />
     </decision-state>
 
     <subflow-state id="DoInboundInterceptSubflow" subflow="intercept">
         <input name="calledAsSubflow" value="true" />
-        <transition on="proceed" to="OutboundContextsAndSecurityParameters" />
+        <transition on="proceed" to="DoDPoPProofValidation" />
     </subflow-state>
 
-    <action-state id="OutboundContextsAndSecurityParameters">
+    <action-state id="ResumeAfterDoDPoPProofValidation">
         <evaluate expression="ValidateClientIDAgainstPolicy" />
         <evaluate expression="ValidateAccessToken" />
         <evaluate expression="ValidateScope" />
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 4dca5e0c..2e3faf09 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
@@ -26,7 +26,9 @@
         p:encryptionOptional="%{idp.oidc.encryptionOptional:true}"
         p:deniedUserInfoAttributes="%{idp.oidc.deniedUserInfoAttributes:}"
         p:issuedClaimsValidator-ref="DefaultUserInfoJWTClaimsValidator"
-        p:unregisteredClientPolicy="#{getObject('shibboleth.oidc.DefaultUnregisteredClientPolicy')}" />
+        p:unregisteredClientPolicy="#{getObject('shibboleth.oidc.DefaultUnregisteredClientPolicy')}"
+        p:dpopProofClaimsValidator-ref="DefaultDPoPProofClaimsValidator"
+        p:dpopProofSignatureValidationConfiguration-ref="DPoPSignatureValidationConfiguration" />
         
     <bean id="OIDC.Registration" parent="AbstractOIDCProfile" lazy-init="true"
         class="net.shibboleth.oidc.profile.config.impl.DefaultOIDCDynamicRegistrationConfiguration"
@@ -417,6 +419,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.Registration.MDDriven" parent="AbstractMDDrivenOIDCFlowAwareProfile" lazy-init="true"
diff --git a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/AbstractOidcApiFlowTest.java b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/AbstractOidcApiFlowTest.java
index 4e1623e7..d83898d5 100644
--- a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/AbstractOidcApiFlowTest.java
+++ b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/AbstractOidcApiFlowTest.java
@@ -24,6 +24,8 @@ import java.util.Collection;
 import java.util.Date;
 import java.util.List;
 
+import org.testng.annotations.BeforeMethod;
+
 import com.nimbusds.jose.Algorithm;
 import com.nimbusds.jose.JOSEException;
 import com.nimbusds.jose.JOSEObjectType;
@@ -38,6 +40,7 @@ import com.nimbusds.jwt.SignedJWT;
 import com.nimbusds.oauth2.sdk.Scope;
 import com.nimbusds.oauth2.sdk.id.ClientID;
 import com.nimbusds.oauth2.sdk.token.BearerAccessToken;
+import com.nimbusds.oauth2.sdk.token.DPoPAccessToken;
 import com.nimbusds.oauth2.sdk.token.RefreshToken;
 import com.nimbusds.openid.connect.sdk.claims.ClaimsSet;
 
@@ -56,6 +59,11 @@ public class AbstractOidcApiFlowTest extends AbstractOidcFlowTest {
         super(flowId);
     }
 
+    @BeforeMethod
+    public void setupRequestURI() {
+        request.setRequestURI("/idp/profile/" + flowId);
+    }
+
     protected BearerAccessToken buildLegacyToken(final String clientId, final String subject, final Scope scope,
             String... consentedClaims)
             throws URISyntaxException, NoSuchAlgorithmException, DataSealerException, ComponentInitializationException {
@@ -158,7 +166,22 @@ public class AbstractOidcApiFlowTest extends AbstractOidcFlowTest {
             return new BearerAccessToken(new PlainJWT(claims.getClaimsSet()).serialize());
         }
     }
-    
+
+    protected DPoPAccessToken buildJWTDPoPToken(final AccessTokenClaimsSet claims, final PrivateKey key,
+            final String alg) throws JOSEException {
+        if (key != null) {
+            final JWSAlgorithm jwsAlgorithm = new JWSAlgorithm(alg);
+            final JWSSigner signer = getSigner(key, jwsAlgorithm);
+            final JWSHeader.Builder headerBuilder =
+                    new JWSHeader.Builder(jwsAlgorithm).type(new JOSEObjectType("at+jwt"));
+            final SignedJWT jwt = new SignedJWT(headerBuilder.build(), claims.getClaimsSet());
+            jwt.sign(signer);
+            return new DPoPAccessToken(jwt.serialize());
+        } else {
+            return new DPoPAccessToken(new PlainJWT(claims.getClaimsSet()).serialize());
+        }
+    }
+
     /**
      * Returns correct implementation of signer based on algorithm type.
      * 
diff --git a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/AbstractOidcClientAuthenticationFlowTest.java b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/AbstractOidcClientAuthenticationFlowTest.java
index 1fbd61ea..99b78738 100644
--- a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/AbstractOidcClientAuthenticationFlowTest.java
+++ b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/AbstractOidcClientAuthenticationFlowTest.java
@@ -26,7 +26,6 @@ import java.util.Map;
 
 import org.opensaml.profile.action.EventIds;
 import org.springframework.webflow.executor.FlowExecutionResult;
-import org.testng.annotations.BeforeMethod;
 import org.testng.annotations.Test;
 
 import com.nimbusds.jose.JOSEException;
@@ -74,11 +73,6 @@ public abstract class AbstractOidcClientAuthenticationFlowTest extends AbstractO
         requestParameters.put("client_assertion_type", "urn:ietf:params:oauth:client-assertion-type:jwt-bearer");
     }
     
-    @BeforeMethod
-    public void setupRequestURI() {
-        request.setRequestURI("/idp/profile/" + flowId);
-    }
-
     @Test
     public void testInvalidSecretJWT_missingSub() throws Exception {
         for (final String id : clientIds) {
diff --git a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/UserInfoTest.java b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/UserInfoTest.java
index b23511fc..2ae1705b 100644
--- a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/UserInfoTest.java
+++ b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/UserInfoTest.java
@@ -18,6 +18,7 @@ import java.io.IOException;
 import java.net.URISyntaxException;
 import java.security.NoSuchAlgorithmException;
 import java.text.ParseException;
+import java.time.Instant;
 import java.util.List;
 
 import javax.annotation.Nonnull;
@@ -40,13 +41,17 @@ import com.nimbusds.jwt.SignedJWT;
 import com.nimbusds.oauth2.sdk.OAuth2Error;
 import com.nimbusds.oauth2.sdk.Scope;
 import com.nimbusds.oauth2.sdk.auth.ClientAuthenticationMethod;
+import com.nimbusds.oauth2.sdk.id.ClientID;
+import com.nimbusds.oauth2.sdk.token.AccessToken;
 import com.nimbusds.oauth2.sdk.token.BearerAccessToken;
 import com.nimbusds.oauth2.sdk.token.BearerTokenError;
+import com.nimbusds.oauth2.sdk.token.DPoPAccessToken;
 import com.nimbusds.openid.connect.sdk.UserInfoSuccessResponse;
 import com.nimbusds.openid.connect.sdk.claims.ClaimsSet;
 import com.nimbusds.openid.connect.sdk.claims.UserInfo;
 
 import net.shibboleth.idp.plugin.oidc.op.storage.RevocationCacheContexts;
+import net.shibboleth.idp.plugin.oidc.op.token.support.AccessTokenClaimsSet;
 import net.shibboleth.oidc.security.credential.JWKCredential;
 import net.shibboleth.shared.component.ComponentInitializationException;
 import net.shibboleth.shared.security.DataSealerException;
@@ -219,6 +224,142 @@ public class UserInfoTest extends AbstractOidcApiFlowTest {
         Assert.assertNull(response.getUserInfoJWT());
     }
 
+    @Test
+    public void testFailWithDPoP_noProof() throws URISyntaxException, NoSuchAlgorithmException, DataSealerException,
+        ComponentInitializationException, IOException {
+        final AccessTokenClaimsSet claims = buildDPoPAccessTokenClaimsSet("mockId");
+        final DPoPAccessToken token = new DPoPAccessToken(claims.serialize(getDataSealer()));
+        storeMetadata(storageService, clientId, "mockSecret", scope);
+        request.addHeader("Authorization", getTokenHeaderValue(token));
+        final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+        assertErrorCode(result, OAuth2Error.INVALID_DPOP_PROOF_CODE);
+    }
+    
+    @Test
+    public void testFailWithDPoPJWT_noProof() throws URISyntaxException, NoSuchAlgorithmException, DataSealerException,
+        ComponentInitializationException, IOException, com.nimbusds.oauth2.sdk.ParseException, JOSEException {
+        final AccessTokenClaimsSet claims = buildDPoPAccessTokenClaimsSet("mockId");
+        final DPoPAccessToken token = buildJWTDPoPToken(claims, signingKey.getPrivateKey(), "RS256");
+        storeMetadata(storageService, clientId, "mockSecret", scope);
+        request.addHeader("Authorization", getTokenHeaderValue(token));
+        final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+        assertErrorCode(result, OAuth2Error.INVALID_DPOP_PROOF_CODE);
+    }
+
+    @SuppressWarnings("null")
+    protected AccessTokenClaimsSet buildDPoPAccessTokenClaimsSet(final String jwkThumbprint) {
+        return new AccessTokenClaimsSet.Builder()
+                .setJWTID(idGenerator)
+                .setClientID(new ClientID(clientId))
+                .setIssuer("https://op.example.org")
+                .setSubject(subject)
+                .setIssuedAt(Instant.now())
+                .setNotBefore(Instant.now().minusSeconds(300))
+                .setExpiresAt(Instant.now().plusSeconds(1800))
+                .setAuthenticationTime(Instant.now())
+                .setScope(new Scope("openid"))
+                .setDpopProofJwkThumbprint(jwkThumbprint)
+                .build();
+    }
+
+    @Test
+    public void testFailWithBearerWithJkt() throws URISyntaxException, NoSuchAlgorithmException, DataSealerException,
+        ComponentInitializationException, IOException {
+        final AccessTokenClaimsSet claims = buildDPoPAccessTokenClaimsSet("mockId");
+        final BearerAccessToken token = new BearerAccessToken(claims.serialize(getDataSealer()));
+        storeMetadata(storageService, clientId, "mockSecret", scope);
+        request.addHeader("Authorization", getTokenHeaderValue(token));
+        final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+        assertErrorCode(result, OAuth2Error.INVALID_DPOP_PROOF_CODE);
+    }
+    
+    @Test
+    public void testFailWithBearerJWTWithJkt() throws URISyntaxException, NoSuchAlgorithmException, DataSealerException,
+        ComponentInitializationException, IOException, com.nimbusds.oauth2.sdk.ParseException, JOSEException {
+        final AccessTokenClaimsSet claims = buildDPoPAccessTokenClaimsSet("mockId");
+        final BearerAccessToken token = buildJWTToken(claims, signingKey.getPrivateKey(), "RS256");
+        storeMetadata(storageService, clientId, "mockSecret", scope);
+        request.addHeader("Authorization", getTokenHeaderValue(token));
+        final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+        assertErrorCode(result, OAuth2Error.INVALID_DPOP_PROOF_CODE);
+    }
+
+    @SuppressWarnings("null")
+    @Test
+    public void testFailWithDPoP_nonMatchingThumbprint() throws URISyntaxException, NoSuchAlgorithmException,
+        DataSealerException, ComponentInitializationException, IOException, com.nimbusds.oauth2.sdk.ParseException,
+        JOSEException {
+        final AccessTokenClaimsSet claims = buildDPoPAccessTokenClaimsSet("mockId");
+        final DPoPAccessToken token = new DPoPAccessToken(claims.serialize(getDataSealer()));
+        request.addHeader("DPoP", buildDPoPProof("POST", "http://localhost/idp/profile/oidc/userinfo").serialize());
+
+        storeMetadata(storageService, clientId, "mockSecret", scope);
+        request.addHeader("Authorization", getTokenHeaderValue(token));
+        final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+        assertErrorCode(result, OAuth2Error.INVALID_DPOP_PROOF_CODE);
+    }
+
+    @SuppressWarnings("null")
+    @Test
+    public void testFailWithDPoPJWT_nonMatchingThumbprint() throws URISyntaxException, NoSuchAlgorithmException,
+        DataSealerException, ComponentInitializationException, IOException, com.nimbusds.oauth2.sdk.ParseException,
+        JOSEException {
+        final AccessTokenClaimsSet claims = buildDPoPAccessTokenClaimsSet("mockId");
+        final DPoPAccessToken token = buildJWTDPoPToken(claims, signingKey.getPrivateKey(), "RS256");
+        request.addHeader("DPoP", buildDPoPProof("POST", "http://localhost/idp/profile/oidc/userinfo").serialize());
+
+        storeMetadata(storageService, clientId, "mockSecret", scope);
+        request.addHeader("Authorization", getTokenHeaderValue(token));
+        final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+        assertErrorCode(result, OAuth2Error.INVALID_DPOP_PROOF_CODE);
+    }
+
+    @SuppressWarnings("null")
+    @Test
+    public void testSuccessOnlySubjectWithDPoPNoAudience() throws URISyntaxException, NoSuchAlgorithmException, DataSealerException,
+        ComponentInitializationException, IOException, com.nimbusds.oauth2.sdk.ParseException, JOSEException {
+        final SignedJWT dpopProof = buildDPoPProof("POST", "http://localhost/idp/profile/oidc/userinfo");
+        final AccessTokenClaimsSet claims =
+                buildDPoPAccessTokenClaimsSet((dpopProof.getHeader().getJWK().computeThumbprint().toString()));
+
+        final DPoPAccessToken token = new DPoPAccessToken(claims.serialize(getDataSealer()));
+        request.addHeader("DPoP", dpopProof.serialize());
+
+        storeMetadata(storageService, clientId, "mockSecret", scope);
+        request.addHeader("Authorization", getTokenHeaderValue(token));
+        final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+        final UserInfoSuccessResponse response = parseSuccessResponse(result, UserInfoSuccessResponse.class);
+        Assert.assertEquals(response.getUserInfo().getSubject().getValue(), subject);
+        final UserInfo userInfo = response.getUserInfo();
+        Assert.assertNotNull(userInfo);
+        Assert.assertNull(userInfo.getEmailAddress());
+        Assert.assertNull(userInfo.getNickname());
+        Assert.assertNull(response.getUserInfoJWT());
+    }
+
+    @SuppressWarnings("null")
+    @Test
+    public void testSuccessOnlySubjectWithDPoPJWTNoAudience() throws URISyntaxException, NoSuchAlgorithmException, DataSealerException,
+        ComponentInitializationException, IOException, com.nimbusds.oauth2.sdk.ParseException, JOSEException {
+        final SignedJWT dpopProof = buildDPoPProof("POST", "http://localhost/idp/profile/oidc/userinfo");
+        final AccessTokenClaimsSet claims =
+                buildDPoPAccessTokenClaimsSet((dpopProof.getHeader().getJWK().computeThumbprint().toString()));
+
+        final DPoPAccessToken token = buildJWTDPoPToken(claims, signingKey.getPrivateKey(), "RS256");
+        request.addHeader("DPoP", dpopProof.serialize());
+
+        storeMetadata(storageService, clientId, "mockSecret", scope);
+        request.addHeader("Authorization", getTokenHeaderValue(token));
+        final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+        final UserInfoSuccessResponse response = parseSuccessResponse(result, UserInfoSuccessResponse.class);
+        Assert.assertEquals(response.getUserInfo().getSubject().getValue(), subject);
+        final UserInfo userInfo = response.getUserInfo();
+        Assert.assertNotNull(userInfo);
+        Assert.assertNull(userInfo.getEmailAddress());
+        Assert.assertNull(userInfo.getNickname());
+        Assert.assertNull(response.getUserInfoJWT());
+    }
+
     @Test
     public void testSuccessOnlySubjectWithJWTAudience() throws URISyntaxException, NoSuchAlgorithmException, DataSealerException,
         ComponentInitializationException, IOException, com.nimbusds.oauth2.sdk.ParseException, JOSEException {
@@ -443,7 +584,7 @@ public class UserInfoTest extends AbstractOidcApiFlowTest {
         };
     }
 
-    @Nonnull private static String getTokenHeaderValue(final BearerAccessToken token) {
+    @Nonnull private static String getTokenHeaderValue(final AccessToken token) {
         final String value = token.toAuthorizationHeader();
         assert value != null;
         return value;
diff --git a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/logic/DPoPAccessTokenInRequestConditionTest.java b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/logic/DPoPAccessTokenInRequestConditionTest.java
new file mode 100644
index 00000000..c895a6e3
--- /dev/null
+++ b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/logic/DPoPAccessTokenInRequestConditionTest.java
@@ -0,0 +1,103 @@
+/*
+ * 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.logic;
+
+import java.util.function.Supplier;
+
+import org.springframework.mock.web.MockHttpServletRequest;
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import jakarta.servlet.http.HttpServletRequest;
+import net.shibboleth.shared.component.ComponentInitializationException;
+import net.shibboleth.shared.component.UninitializedComponentException;
+import net.shibboleth.shared.primitive.NonnullSupplier;
+
+/**
+ * Unit tests for {@link DPoPAccessTokenInRequestCondition}.
+ */
+public class DPoPAccessTokenInRequestConditionTest {
+
+    DPoPAccessTokenInRequestCondition condition;
+
+    @Test(expectedExceptions = ComponentInitializationException.class)
+    public void testNoSupplier() throws ComponentInitializationException {
+        condition = new DPoPAccessTokenInRequestCondition();
+        condition.setId("mockId");
+        condition.initialize();
+    }
+
+    @Test(expectedExceptions = UninitializedComponentException.class)
+    public void testNotInitialized(){
+        condition = new DPoPAccessTokenInRequestCondition();
+        condition.setHttpServletRequestSupplier(new Supplier<>() {
+
+            @Override
+            public HttpServletRequest get() {
+                return null;
+            }
+            
+        });
+        condition.test(null);
+    }
+
+    @Test
+    public void testNoValues() {
+        condition = initWithSupplier();
+        Assert.assertFalse(condition.test(null));
+    }
+
+    @Test
+    public void testOneDPoPValue() {
+        condition = initWithSupplier("DPoP mock");
+        Assert.assertTrue(condition.test(null));
+    }
+
+    @Test
+    public void testOneBearerValue() {
+        condition = initWithSupplier("Bearer mock");
+        Assert.assertFalse(condition.test(null));
+    }
+
+    public void testMultioleValues() {
+        condition = initWithSupplier("Bearer mock", "DPoP mock");
+        Assert.assertTrue(condition.test(null));
+    }
+
+    protected DPoPAccessTokenInRequestCondition initWithSupplier(final String... values) {
+        final NonnullSupplier<HttpServletRequest> supplier = new NonnullSupplier<>() {
+
+            @Override
+            public HttpServletRequest get() {
+                final MockHttpServletRequest httpRequest = new MockHttpServletRequest();
+                for (final String value : values) {
+                    assert value != null;
+                    httpRequest.addHeader("Authorization", value);
+                }
+                return httpRequest;
+            }
+            
+        };
+        condition = new DPoPAccessTokenInRequestCondition();
+        condition.setHttpServletRequestSupplier(supplier);
+        condition.setId("mockId");
+        try {
+            condition.initialize();
+        } catch (ComponentInitializationException e) {
+            Assert.fail();
+        }
+        return condition;
+    }
+}

-- 
To stop receiving notification emails like this one, please contact
the administrator of this repository.


More information about the commits mailing list