[java-idp-plugin-oidc-rp] branch main updated: JOIDCRP-30 - Support PCKE (RFC7636)

Phil Smart philip.smart at jisc.ac.uk
Fri Sep 29 16:04:26 UTC 2023


This is an automated email from the git hooks/post-receive script.

philsmart pushed a commit to branch main
in repository java-idp-plugin-oidc-rp.

View the commit online:
http://git.shibboleth.net/view/?p=java-idp-plugin-oidc-rp.git;a=commit;h=65171b311cc3faf45b0644fbb20d481f72633101

The following commit(s) were added to refs/heads/main by this push:
     new 65171b3  JOIDCRP-30 - Support PCKE (RFC7636)
65171b3 is described below

commit 65171b311cc3faf45b0644fbb20d481f72633101
Author: Phil Smart <philip.smart at jisc.ac.uk>
AuthorDate: Mon Aug 28 21:47:19 2023 +0100

    JOIDCRP-30 - Support PCKE (RFC7636)
    
    https://shibboleth.atlassian.net/browse/JOIDCRP-30
---
 .../impl/DefaultAuthCodeTokenRequestEncoder.java   |   6 +-
 .../impl/AddPCKECodeVerifierAndChallenge.java      | 128 +++++++++++++++++++++
 .../oidc-relying-party-authn-beans.xml             |   2 +
 .../authn/oidc/rp/impl/AbstractOIDCTest.java       |   6 +-
 .../impl/AddPCKECodeVerifierAndChallengeTest.java  |  81 +++++++++++++
 5 files changed, 220 insertions(+), 3 deletions(-)

diff --git a/idp-oidc-rp-impl/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/encoding/impl/DefaultAuthCodeTokenRequestEncoder.java b/idp-oidc-rp-impl/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/encoding/impl/DefaultAuthCodeTokenRequestEncoder.java
index 4c53b01..7747762 100644
--- a/idp-oidc-rp-impl/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/encoding/impl/DefaultAuthCodeTokenRequestEncoder.java
+++ b/idp-oidc-rp-impl/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/encoding/impl/DefaultAuthCodeTokenRequestEncoder.java
@@ -28,6 +28,7 @@ import com.nimbusds.oauth2.sdk.AuthorizationCodeGrant;
 import com.nimbusds.oauth2.sdk.AuthorizationGrant;
 import com.nimbusds.oauth2.sdk.TokenRequest;
 import com.nimbusds.oauth2.sdk.http.HTTPRequest;
+import com.nimbusds.oauth2.sdk.pkce.CodeVerifier;
 import com.nimbusds.openid.connect.sdk.AuthenticationSuccessResponse;
 import com.nimbusds.openid.connect.sdk.op.OIDCProviderMetadata;
 
@@ -65,8 +66,11 @@ public class DefaultAuthCodeTokenRequestEncoder extends AbstractRequestEncoderFu
                 return null;
             }
             
+            // If PKCE was set in the request (is not null) use it, else set it to null
             final AuthorizationGrant codeGrant = 
-                    new AuthorizationCodeGrant(authnResponse.getAuthorizationCode(), authnRequest.getRedirectURI());
+                    new AuthorizationCodeGrant(authnResponse.getAuthorizationCode(), authnRequest.getRedirectURI(),
+                            authnRequest.getCodeVerifier() != null ? new CodeVerifier(authnRequest.getCodeVerifier()) 
+                                    : null);
             
             final TokenRequest tokenRequest = new TokenRequest(providerMetadata.getTokenEndpointURI(),
                             authnContext.getClientAuthentication(), codeGrant);
diff --git a/idp-oidc-rp-impl/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/messaging/impl/AddPCKECodeVerifierAndChallenge.java b/idp-oidc-rp-impl/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/messaging/impl/AddPCKECodeVerifierAndChallenge.java
new file mode 100644
index 0000000..dab0177
--- /dev/null
+++ b/idp-oidc-rp-impl/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/messaging/impl/AddPCKECodeVerifierAndChallenge.java
@@ -0,0 +1,128 @@
+/*
+ * 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.authn.oidc.rp.messaging.impl;
+
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.security.SecureRandom;
+
+import javax.annotation.Nonnull;
+
+import org.opensaml.messaging.context.MessageContext;
+import org.opensaml.messaging.handler.MessageHandlerException;
+import org.slf4j.Logger;
+
+import net.shibboleth.oidc.profile.core.OAuthAuthorizationRequest.CodeChallengeMethod;
+import net.shibboleth.shared.annotation.constraint.NotEmpty;
+import net.shibboleth.shared.codec.Base64Support;
+import net.shibboleth.shared.codec.EncodingException;
+import net.shibboleth.shared.primitive.LoggerFactory;
+
+/**
+ * Create an OAuth 2.0 PCKE code_verifier to use in the token request, and derives a code_challenge for immediate use in
+ * the authorization request.
+ */
+public class AddPCKECodeVerifierAndChallenge extends AbstractOIDCAuthenticationRequestActionMessageHandler {
+    
+    /** Logger. */
+    @Nonnull private final Logger log = LoggerFactory.getLogger(AddPCKECodeVerifierAndChallenge.class);
+
+    @Override
+    protected void doInvoke(final MessageContext messageContext) throws MessageHandlerException {
+        
+        if (getProfileConfiguration().isForcePKCE(lookupProfileRequestContext(messageContext))) {
+            
+            log.trace("{} PKCE enabled, adding code_challenge to request", getLogPrefix());
+            
+            final CodeChallengeMethod method = 
+                    getProfileConfiguration().isAllowPKCEPlain(lookupProfileRequestContext(messageContext)) 
+                    ? CodeChallengeMethod.PLAIN :  CodeChallengeMethod.S256;
+            
+            final String codeVerifier = generateCodeVerifier(32);     
+            final String challenge = computeCodeChallenge(codeVerifier, method);
+            
+            if (log.isTraceEnabled()) {
+                log.trace("{} Created code verifier '...{}'", getLogPrefix(), 
+                        codeVerifier.substring(challenge.length()-3));
+                log.trace("{} Derived code challenge '...{}'", getLogPrefix(), 
+                        challenge.substring(challenge.length()-3));
+            }
+            getAuthenticationRequest().setCodeVerifier(codeVerifier);
+            getAuthenticationRequest().setCodeChallenge(challenge);
+            getAuthenticationRequest().setCodeChallengeMethod(method);
+
+        } else {            
+            log.trace("{} PKCE not enabled", getLogPrefix());            
+        }        
+    }
+    
+    /**
+     * Generates a code_verifier for use during Proof Key for Code Exchange. The generated bytes are base64 URL 
+     * encoded before they are returned. 
+     *  
+     * @param length the byte length of the code_verifier. Must be at least 32 bytes long (RFC7636 section 7.1).
+     * 
+     * @return the base64 URL encoded coder_verifier value.
+     * 
+     * @throws MessageHandlerException if there is an error generating the verifier. 
+     */
+    @Nonnull private static String generateCodeVerifier(@Nonnull final Integer length) throws MessageHandlerException {
+        if (length < 32) {
+            throw new MessageHandlerException("PKCE coder_verifier must be at least 32 bytes long");
+        }
+        try {
+            final SecureRandom secureRandom = new SecureRandom();
+            final byte[] verifierInBytes = new byte[length];
+            secureRandom.nextBytes(verifierInBytes);
+            return Base64Support.encodeURLSafe(verifierInBytes);
+        } catch (final Exception e) {
+            throw new MessageHandlerException(e);            
+        }
+    }
+    
+    /**
+     * Compute the code_challenge from the code_verifier. If the {@link CodeChallengeMethod#PLAIN} method is used, the
+     * codeVerifier is returned directly. If the {@link CodeChallengeMethod#S256} method is used, the bytes of the
+     * codeVerifier are SHA-256 hashed and base 64 URL encoded before being returned. 
+     * 
+     * @param codeVerifier the code_verifier to compute the code_challenge from
+     * @param method the code_challenge_method
+     * 
+     * @return the computed code_challenge
+     * 
+     * @throws MessageHandlerException on error computing the code_challenge
+     */
+    @Nonnull @NotEmpty private String computeCodeChallenge(@Nonnull @NotEmpty final String codeVerifier, 
+            @Nonnull final CodeChallengeMethod method) throws MessageHandlerException {
+        
+        if (method == CodeChallengeMethod.PLAIN) {
+            return codeVerifier;
+        }
+        try {            
+            final MessageDigest md = MessageDigest.getInstance("SHA-256");
+            final byte[] hash = md.digest(codeVerifier.getBytes());
+            assert hash != null;
+            return Base64Support.encodeURLSafe(hash);
+            
+        } catch (final NoSuchAlgorithmException | EncodingException e) {
+            throw new MessageHandlerException("Unable to compute code_challenge", e);
+        }
+
+        
+        
+    }
+
+}
diff --git a/idp-oidc-rp-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/authn/OIDCRelyingParty/oidc-relying-party-authn-beans.xml b/idp-oidc-rp-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/authn/OIDCRelyingParty/oidc-relying-party-authn-beans.xml
index ace2305..0b2c168 100644
--- a/idp-oidc-rp-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/authn/OIDCRelyingParty/oidc-relying-party-authn-beans.xml
+++ b/idp-oidc-rp-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/authn/OIDCRelyingParty/oidc-relying-party-authn-beans.xml
@@ -117,6 +117,8 @@
                         <bean id="AddRequestedClaims" scope="prototype"
                             class="net.shibboleth.idp.plugin.authn.oidc.rp.messaging.impl.AddRequestedClaimsHandler"
                             p:requestedClaimsHook="#{getObject('shibboleth.authn.oidc.rp.RequestedClaimsHook')}" />
+                        <bean id="AddPCKECodeVerifierAndChallenge" scope="prototype"
+                            class="net.shibboleth.idp.plugin.authn.oidc.rp.messaging.impl.AddPCKECodeVerifierAndChallenge"/>
                         <bean id="AddRedirectURI" scope="prototype"
                             class="net.shibboleth.idp.plugin.authn.oidc.rp.messaging.impl.AddRedirectURIHandler"
                             p:httpServletRequestSupplier-ref="shibboleth.HttpServletRequestSupplier"
diff --git a/idp-oidc-rp-impl/src/test/java/net/shibboleth/idp/plugin/authn/oidc/rp/impl/AbstractOIDCTest.java b/idp-oidc-rp-impl/src/test/java/net/shibboleth/idp/plugin/authn/oidc/rp/impl/AbstractOIDCTest.java
index f7f8328..8e8dfcc 100644
--- a/idp-oidc-rp-impl/src/test/java/net/shibboleth/idp/plugin/authn/oidc/rp/impl/AbstractOIDCTest.java
+++ b/idp-oidc-rp-impl/src/test/java/net/shibboleth/idp/plugin/authn/oidc/rp/impl/AbstractOIDCTest.java
@@ -326,7 +326,8 @@ public abstract class AbstractOIDCTest {
      * @param prc the prc
      * @return the relying party configuration
      */
-    protected DefaultOIDCAuthorizationConfiguration getRelyingPartyProfileConfig(@Nonnull final ProfileRequestContext prc) {
+    @Nullable protected DefaultOIDCAuthorizationConfiguration getRelyingPartyProfileConfig(
+            @Nonnull final ProfileRequestContext prc) {
         final var rpc = prc.getSubcontext(RelyingPartyContext.class);
         assert rpc != null;
         return (DefaultOIDCAuthorizationConfiguration) rpc.getProfileConfig();
@@ -340,7 +341,8 @@ public abstract class AbstractOIDCTest {
      * 
      * @return the message context. Never null at that point.
      */
-    @Nonnull protected MessageContext getOutboundMessageContextFailIfNull(@Nullable final ProfileRequestContext prc){
+    @Nonnull protected MessageContext getOutboundMessageContextFailIfNull(
+            @Nullable final ProfileRequestContext prc){
         assertNotNull(prc);
         assert prc != null;
         final var outboundMsgCtx = prc.getOutboundMessageContext();
diff --git a/idp-oidc-rp-impl/src/test/java/net/shibboleth/idp/plugin/authn/oidc/rp/messaging/impl/AddPCKECodeVerifierAndChallengeTest.java b/idp-oidc-rp-impl/src/test/java/net/shibboleth/idp/plugin/authn/oidc/rp/messaging/impl/AddPCKECodeVerifierAndChallengeTest.java
new file mode 100644
index 0000000..8d63700
--- /dev/null
+++ b/idp-oidc-rp-impl/src/test/java/net/shibboleth/idp/plugin/authn/oidc/rp/messaging/impl/AddPCKECodeVerifierAndChallengeTest.java
@@ -0,0 +1,81 @@
+/*
+ * Licensed to the University Corporation for Advanced Internet Development,
+ * Inc. (UCAID) under one or more contributor license agreements.  See the
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID licenses this file to You 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.authn.oidc.rp.messaging.impl;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertNotNull;
+import static org.testng.Assert.assertNull;
+
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import net.shibboleth.idp.plugin.authn.oidc.rp.impl.AbstractOIDCTest;
+
+/**
+ * Tests for {@link AddPCKECodeVerifierAndChallenge}.
+ */
+public class AddPCKECodeVerifierAndChallengeTest extends AbstractOIDCTest {
+    
+    /** The action to test.*/
+    private AddPCKECodeVerifierAndChallenge handler;
+    
+    @Override
+    @BeforeMethod
+    public void setup() throws Exception {
+        super.setup();
+        handler = new AddPCKECodeVerifierAndChallenge();         
+    }
+    
+    @Test
+    public void testSuccessS256() throws Exception {
+        partyConfig.setForcePKCE(true);
+        partyConfig.setAllowPKCEPlain(false);
+        handler.initialize();        
+        final var outboundMsgCtx = getOutboundMessageContextFailIfNull(prc);
+        handler.invoke(outboundMsgCtx);
+        
+        assertNotNull(authnRequest.getCodeVerifier());
+        assertNotNull(authnRequest.getCodeChallenge());
+    }
+    
+    @Test
+    public void testSuccessDisabled() throws Exception {
+        partyConfig.setForcePKCE(false);
+        partyConfig.setAllowPKCEPlain(false);
+        handler.initialize();        
+        final var outboundMsgCtx = getOutboundMessageContextFailIfNull(prc);
+        handler.invoke(outboundMsgCtx);
+        
+        assertNull(authnRequest.getCodeVerifier());
+        assertNull(authnRequest.getCodeChallenge());
+    }
+    
+    @Test
+    public void testSuccessPlain() throws Exception {
+        partyConfig.setForcePKCE(true);
+        partyConfig.setAllowPKCEPlain(true);
+        handler.initialize();        
+        final var outboundMsgCtx = getOutboundMessageContextFailIfNull(prc);
+        handler.invoke(outboundMsgCtx);
+        
+        assertNotNull(authnRequest.getCodeVerifier());
+        assertNotNull(authnRequest.getCodeChallenge());
+        assertEquals(authnRequest.getCodeVerifier(), authnRequest.getCodeChallenge());
+    }
+
+}

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


More information about the commits mailing list