[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