[java-idp-oidc] 01/02: JOIDC-201 - Support for OAuth 2.0 Demonstrating Proof of Possession (DPoP)
Henri Mikkonen
henri.mikkonen at iki.fi
Fri Oct 11 08:05:59 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=575241a046042b28c973863409a63ce311a311e2
commit 575241a046042b28c973863409a63ce311a311e2
Author: Henri Mikkonen <henri.mikkonen at iki.fi>
AuthorDate: Fri Oct 11 11:02:54 2024 +0300
JOIDC-201 - Support for OAuth 2.0 Demonstrating Proof of Possession (DPoP)
https://shibboleth.atlassian.net/browse/JOIDC-201
Corrected the default DPoP proof ath-claim validation to tackle introspection/revocation
---
...oPAccessTokenHashFromRequestLookupFunction.java | 53 ++++++++++++++++++----
.../META-INF/net.shibboleth.idp/postconfig.xml | 6 +--
.../op/profile/flow/AbstractOidcApiFlowTest.java | 35 ++++++++++++++
.../op/profile/flow/IntrospectionFlowTest.java | 46 +++++++++++++++++++
.../oidc/op/profile/flow/RevocationFlowTest.java | 41 +++++++++++++++++
.../plugin/oidc/op/profile/flow/UserInfoTest.java | 16 +------
6 files changed, 169 insertions(+), 28 deletions(-)
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/logic/DPoPAccessTokenHashFromRequestLookupFunction.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/logic/DPoPAccessTokenHashFromRequestLookupFunction.java
index d601bc7e..09f9be98 100644
--- a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/logic/DPoPAccessTokenHashFromRequestLookupFunction.java
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/logic/DPoPAccessTokenHashFromRequestLookupFunction.java
@@ -27,6 +27,8 @@ import org.slf4j.Logger;
import com.nimbusds.jose.JOSEException;
import com.nimbusds.jose.util.Base64URL;
import com.nimbusds.oauth2.sdk.ParseException;
+import com.nimbusds.oauth2.sdk.TokenIntrospectionRequest;
+import com.nimbusds.oauth2.sdk.TokenRevocationRequest;
import com.nimbusds.oauth2.sdk.dpop.DPoPUtils;
import com.nimbusds.oauth2.sdk.token.DPoPAccessToken;
@@ -38,7 +40,9 @@ import net.shibboleth.shared.logic.Constraint;
import net.shibboleth.shared.primitive.LoggerFactory;
/**
- * A function that calculates a hash of the DPoP access token from the {@link HttpServletRequest} if found.
+ * A function that calculates a hash of the DPoP access token from the {@link HttpServletRequest} if found. The token
+ * value is fetched via Authorization header or via token token-parameter if the inbound message is an introspection or
+ * revocation request.
*
* @since 4.2.0
*/
@@ -72,28 +76,57 @@ public class DPoPAccessTokenHashFromRequestLookupFunction extends AbstractIdenti
@Override @Nullable
public String apply(final @Nullable ProfileRequestContext input) {
checkComponentActive();
+
final HttpServletRequest httpServletRequest = httpServletRequestSupplier.get();
if (httpServletRequest == null) {
return null;
}
+
+ final ProfileRequestContext profileRequestContext = input != null ? input :
+ (ProfileRequestContext) httpServletRequest.getAttribute(ProfileRequestContext.BINDING_KEY);
+ if (profileRequestContext != null && profileRequestContext.getInboundMessageContext() != null) {
+ final Object message = profileRequestContext.ensureInboundMessageContext().getMessage();
+ if (message instanceof TokenRevocationRequest revocationRequest) {
+ return calculateHash(new DPoPAccessToken(revocationRequest.getToken().getValue()));
+ }
+ if (message instanceof TokenIntrospectionRequest introspectionRequest) {
+ return calculateHash(new DPoPAccessToken(introspectionRequest.getToken().getValue()));
+ }
+ }
+
final Enumeration<String> authorizationHeaders = httpServletRequest.getHeaders("Authorization");
if (authorizationHeaders == null) {
return null;
}
while (authorizationHeaders.hasMoreElements()) {
try {
- final DPoPAccessToken token = DPoPAccessToken.parse(authorizationHeaders.nextElement());
- if (token != null) {
- final Base64URL hash = DPoPUtils.computeSHA256(token);
- if (hash != null) {
- return hash.toString();
- }
+ final String hash = calculateHash(DPoPAccessToken.parse(authorizationHeaders.nextElement()));
+ if (hash != null) {
+ return hash;
}
} catch (final ParseException e) {
- // ignore, could not parse the DPoPAccessToken
- } catch (final JOSEException e) {
- log.warn("Could not compute SHA256 hash for the DPoP access token", e);
+ log.trace("Could not parse DPoP access token {}", e.getMessage());
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Calculates the SHA-256 hash of the given DPoP access token.
+ *
+ * @param token the DPoP access token
+ * @return the SHA-256 hash value or null if it cannot be calculated
+ */
+ protected String calculateHash(final DPoPAccessToken token) {
+ try {
+ if (token != null) {
+ final Base64URL hash = DPoPUtils.computeSHA256(token);
+ if (hash != null) {
+ return hash.toString();
+ }
}
+ } catch (final JOSEException e) {
+ log.warn("Could not compute SHA256 hash for the DPoP access token", e);
}
return null;
}
diff --git a/idp-oidc-extension-impl/src/main/resources/META-INF/net.shibboleth.idp/postconfig.xml b/idp-oidc-extension-impl/src/main/resources/META-INF/net.shibboleth.idp/postconfig.xml
index 41737386..f031dd2d 100644
--- a/idp-oidc-extension-impl/src/main/resources/META-INF/net.shibboleth.idp/postconfig.xml
+++ b/idp-oidc-extension-impl/src/main/resources/META-INF/net.shibboleth.idp/postconfig.xml
@@ -694,11 +694,11 @@
value="#{T(com.nimbusds.oauth2.sdk.OAuth2Error).INVALID_SCOPE}" />
<entry key="#{T(net.shibboleth.oidc.profile.core.OidcEventIds).INVALID_DPOP_PROOF}"
- value="#{T(com.nimbusds.oauth2.sdk.OAuth2Error).INVALID_DPOP_PROOF_CODE}" />
+ value="#{T(com.nimbusds.oauth2.sdk.OAuth2Error).INVALID_DPOP_PROOF}" />
<entry key="#{T(net.shibboleth.oidc.profile.core.OidcEventIds).MISSING_DPOP_PROOF}"
- value="#{T(com.nimbusds.oauth2.sdk.OAuth2Error).INVALID_DPOP_PROOF_CODE}" />
+ value="#{T(com.nimbusds.oauth2.sdk.OAuth2Error).INVALID_DPOP_PROOF}" />
<entry key="#{T(net.shibboleth.oidc.profile.core.OidcEventIds).INVALID_DPOP_NONCE}"
- value="#{T(com.nimbusds.oauth2.sdk.OAuth2Error).USE_DPOP_NONCE_CODE}" />
+ value="#{T(com.nimbusds.oauth2.sdk.OAuth2Error).USE_DPOP_NONCE}" />
<!-- Missing from Nimbus. -->
<entry key="#{T(net.shibboleth.oidc.profile.core.OidcEventIds).INVALID_TARGET}"
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 8781dc01..0d1b38b7 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
@@ -223,4 +223,39 @@ public class AbstractOidcApiFlowTest extends AbstractOidcFlowTest {
return null;
}
+ @SuppressWarnings("null")
+ protected AccessTokenClaimsSet buildDPoPAccessTokenClaimsSet(final String jwkThumbprint,
+ final String clientId, final String subject) {
+ 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();
+ }
+
+ @SuppressWarnings("null")
+ protected AccessTokenClaimsSet buildDPoPAccessTokenClaimsSet(final String jwkThumbprint,
+ final String clientId, final String subject, final String id, final String rootId) {
+ return new AccessTokenClaimsSet.Builder()
+ .setJWTID(id)
+ .setRootTokenIdentifier(rootId)
+ .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();
+ }
+
}
\ No newline at end of file
diff --git a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/IntrospectionFlowTest.java b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/IntrospectionFlowTest.java
index f6f757e7..518cdc4a 100644
--- a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/IntrospectionFlowTest.java
+++ b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/IntrospectionFlowTest.java
@@ -36,7 +36,9 @@ import org.testng.annotations.Test;
import com.nimbusds.jose.JOSEException;
import com.nimbusds.jose.JWSAlgorithm;
+import com.nimbusds.jose.jwk.ECKey;
import com.nimbusds.jwt.JWT;
+import com.nimbusds.jwt.SignedJWT;
import com.nimbusds.oauth2.sdk.OAuth2Error;
import com.nimbusds.oauth2.sdk.Scope;
import com.nimbusds.oauth2.sdk.TokenIntrospectionErrorResponse;
@@ -47,6 +49,7 @@ import com.nimbusds.oauth2.sdk.id.Audience;
import com.nimbusds.oauth2.sdk.id.ClientID;
import com.nimbusds.oauth2.sdk.token.AccessTokenType;
import com.nimbusds.oauth2.sdk.token.BearerAccessToken;
+import com.nimbusds.oauth2.sdk.token.DPoPAccessToken;
import net.shibboleth.idp.plugin.oidc.op.storage.RevocationCacheContexts;
import net.shibboleth.idp.plugin.oidc.op.token.support.AccessTokenClaimsSet;
@@ -86,6 +89,7 @@ public class IntrospectionFlowTest extends AbstractOidcClientAuthenticationFlowT
public void tearDown() throws IOException {
removeMetadata(storageService, clientId);
removeMetadata(storageService, clientIdNotMDDriven);
+ removeMetadata(storageService, clientIdDPoPProofEnforced);
}
@Test
@@ -233,6 +237,48 @@ public class IntrospectionFlowTest extends AbstractOidcClientAuthenticationFlowT
}
}
+ @Test
+ public void testFailureDPoP_missingMandatoryProof() throws IOException, NoSuchAlgorithmException,
+ URISyntaxException, DataSealerException, ComponentInitializationException {
+ final String clientId = clientIdDPoPProofEnforced;
+ storeMetadata(storageService, clientId, clientSecret, scope);
+ setBasicAuth(clientId, clientSecret);
+ setHttpFormRequest("POST", Map.of(
+ "token",
+ new BearerAccessToken(buildDefaultDPoPClaimsSet(clientId, "sub", null, "mockJkt")
+ .serialize(getDataSealer())).toJSONObject().getAsString("access_token"),
+ "token_type",
+ "access_token"));
+ final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+ assertErrorCode(result, OAuth2Error.INVALID_DPOP_PROOF_CODE);
+ }
+
+ @SuppressWarnings("null")
+ @Test
+ public void testSuccessDPoP_withMandatoryProof() throws IOException, NoSuchAlgorithmException, URISyntaxException,
+ DataSealerException, ComponentInitializationException, JOSEException {
+ final String clientId = clientIdDPoPProofEnforced;
+ final ECKey dpopProofKey = defaultDPoPProofKey();
+ final String jkt = dpopProofKey.computeThumbprint().toString();
+ final AccessTokenClaimsSet claims = buildDPoPAccessTokenClaimsSet(jkt, clientId, "jdoe at example.org");
+
+ final DPoPAccessToken token = new DPoPAccessToken(claims.serialize(getDataSealer()));
+ final SignedJWT dpopProof = buildDPoPProof(dpopProofKey, JWSAlgorithm.ES256, "POST",
+ "http://localhost/idp/profile/oauth2/introspection", token, createValidDPoPNonce());
+ request.addHeader("DPoP", dpopProof.serialize());
+ setBasicAuth(clientId, clientSecret);
+ storeMetadata(storageService, clientId, clientSecret, scope);
+ setHttpFormRequest("POST", Collections.singletonMap("token", token.getValue()));
+ final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+ final TokenIntrospectionSuccessResponse resp =
+ parseSuccessResponse(result, TokenIntrospectionSuccessResponse.class);
+ Assert.assertTrue(resp.isActive());
+ assertDPoPToken(resp, jkt);
+ Assert.assertEquals(resp.getClientID().getValue(), clientId);
+ Assert.assertEquals(resp.getScope(), Scope.parse("openid"));
+ Assert.assertNull(resp.getAudience());
+ }
+
@Test
public void testSuccessWithPostAuth() throws IOException, NoSuchAlgorithmException, URISyntaxException,
DataSealerException, ComponentInitializationException {
diff --git a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/RevocationFlowTest.java b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/RevocationFlowTest.java
index 6021c7cf..20f12031 100644
--- a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/RevocationFlowTest.java
+++ b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/flow/RevocationFlowTest.java
@@ -35,12 +35,15 @@ import org.testng.annotations.Test;
import com.nimbusds.jose.JOSEException;
import com.nimbusds.jose.JWSAlgorithm;
+import com.nimbusds.jose.jwk.ECKey;
import com.nimbusds.jwt.JWT;
+import com.nimbusds.jwt.SignedJWT;
import com.nimbusds.oauth2.sdk.OAuth2Error;
import com.nimbusds.oauth2.sdk.ParseException;
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.DPoPAccessToken;
import net.shibboleth.idp.plugin.oidc.op.oauth2.messaging.impl.OAuth2RevocationSuccessResponse;
import net.shibboleth.idp.plugin.oidc.op.storage.RevocationCacheContexts;
@@ -84,6 +87,7 @@ public class RevocationFlowTest extends AbstractOidcClientAuthenticationFlowTest
removeMetadata(storageService, clientId);
removeMetadata(storageService, clientIdNotMDDriven);
removeMetadata(storageService, clientIdSingle);
+ removeMetadata(storageService, clientIdDPoPProofEnforced);
}
@Test
@@ -168,6 +172,43 @@ public class RevocationFlowTest extends AbstractOidcClientAuthenticationFlowTest
Assert.assertFalse(revocationCache.isRevoked(RevocationCacheContexts.AUTHORIZATION_CODE, rootId));
}
+ @Test
+ public void testFailureSingleDPoPAccessToken_missingMandatoryProof() throws IOException, NoSuchAlgorithmException,
+ URISyntaxException, DataSealerException, ComponentInitializationException {
+ final String clientId = clientIdDPoPProofEnforced;
+ final AccessTokenClaimsSet claims = buildDPoPAccessTokenClaimsSet("mockId", clientId, "jdoe at example.org");
+ final DPoPAccessToken token = new DPoPAccessToken(claims.serialize(getDataSealer()));
+ setBasicAuth(clientId, clientSecret);
+ storeMetadata(storageService, clientId, clientSecret, scope);
+ setHttpFormRequest("POST", Collections.singletonMap("token", token.getValue()));
+ final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+ assertErrorCode(result, OAuth2Error.INVALID_DPOP_PROOF_CODE);
+ }
+
+ @SuppressWarnings("null")
+ @Test
+ public void testSuccessSingleAccessToken_withMandatoryProof() throws IOException, NoSuchAlgorithmException,
+ URISyntaxException, DataSealerException, ComponentInitializationException, JOSEException {
+ final String id = idGenerator.generateIdentifier(false);
+ final String rootId = idGenerator.generateIdentifier(false);
+ final String clientId = clientIdDPoPProofEnforced;
+ final ECKey dpopProofKey = defaultDPoPProofKey();
+ final AccessTokenClaimsSet claims =
+ buildDPoPAccessTokenClaimsSet(dpopProofKey.computeThumbprint().toString(), clientId,
+ "jdoe at example.org", id, rootId);
+
+ final DPoPAccessToken token = new DPoPAccessToken(claims.serialize(getDataSealer()));
+ final SignedJWT dpopProof = buildDPoPProof(dpopProofKey, JWSAlgorithm.ES256, "POST",
+ "http://localhost/idp/profile/oauth2/revocation", token, createValidDPoPNonce());
+ request.addHeader("DPoP", dpopProof.serialize());
+ setBasicAuth(clientId, clientSecret);
+ storeMetadata(storageService, clientId, clientSecret, scope);
+ setHttpFormRequest("POST", Collections.singletonMap("token", token.getValue()));
+ final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+ parseSuccessResponse(result, OAuth2RevocationSuccessResponse.class);
+ Assert.assertTrue(revocationCache.isRevoked(RevocationCacheContexts.AUTHORIZATION_CODE, rootId));
+ }
+
@Test
public void testSuccessSingleRefreshToken() throws IOException, NoSuchAlgorithmException, URISyntaxException,
DataSealerException, ComponentInitializationException {
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 8bc83da6..c44c4714 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,7 +18,6 @@ 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;
@@ -44,7 +43,6 @@ import com.nimbusds.oauth2.sdk.OAuth2Error;
import com.nimbusds.oauth2.sdk.Response;
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;
@@ -258,20 +256,8 @@ public class UserInfoTest extends AbstractOidcApiFlowTest {
assertInvalidDPoPToken(result);
}
- @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();
+ return buildDPoPAccessTokenClaimsSet(jwkThumbprint, clientId, subject);
}
@Test
--
To stop receiving notification emails like this one, please contact
the administrator of this repository.
More information about the commits
mailing list