[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