[java-idp-oidc] branch main updated: JOIDC-97 - support C_HASH in ID_Token also for Authorization Code Flow with PKCE

Henri Mikkonen henri.mikkonen at iki.fi
Fri Jun 10 13:34:41 UTC 2022


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=ef14718b716e9f0da6151a7d629628c8b8fb3941

The following commit(s) were added to refs/heads/main by this push:
     new ef14718b JOIDC-97 - support C_HASH in ID_Token also for Authorization Code Flow with PKCE
ef14718b is described below

commit ef14718b716e9f0da6151a7d629628c8b8fb3941
Author: Henri Mikkonen <henri.mikkonen at iki.fi>
AuthorDate: Fri Jun 10 16:34:25 2022 +0300

    JOIDC-97 - support C_HASH in ID_Token also for Authorization Code Flow with PKCE
    
    https://shibboleth.atlassian.net/browse/JOIDC-97
    
    Refactored AddAuthorizationCodeHashToIDToken to exploit newly added helper functions
    when calculating the c_hash value. The helper functions may be used together with the
    IDTokenManipulationStrategy (JOIDC-104) in order to include c_hash value to the id_token
    also when it's not mandated by the spec.
---
 ...efaultComputeAuthorizationCodeHashFunction.java | 127 +++++++++++++++++++++
 ...onseContextAuthorizationCodeLookupFunction.java |  50 ++++++++
 ...okenRequestAuthorizationCodeLookupFunction.java |  44 +++++++
 .../impl/AddAuthorizationCodeHashToIDToken.java    |  54 ++++++---
 .../idp/flows/oidc/authorize/authorize-beans.xml   |  12 ++
 .../AddAuthorizationCodeHashToIDTokenTest.java     |  11 +-
 6 files changed, 281 insertions(+), 17 deletions(-)

diff --git a/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/context/navigate/DefaultComputeAuthorizationCodeHashFunction.java b/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/context/navigate/DefaultComputeAuthorizationCodeHashFunction.java
new file mode 100644
index 00000000..d8ed9175
--- /dev/null
+++ b/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/context/navigate/DefaultComputeAuthorizationCodeHashFunction.java
@@ -0,0 +1,127 @@
+/*
+ * 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.oidc.op.profile.context.navigate;
+
+import java.util.function.Function;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.messaging.context.navigate.ContextDataLookupFunction;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.opensaml.xmlsec.SignatureSigningParameters;
+import org.opensaml.xmlsec.context.SecurityParametersContext;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.nimbusds.jose.JWSAlgorithm;
+import com.nimbusds.oauth2.sdk.AuthorizationCode;
+import com.nimbusds.openid.connect.sdk.claims.CodeHash;
+
+import net.shibboleth.utilities.java.support.annotation.constraint.NonnullAfterInit;
+import net.shibboleth.utilities.java.support.component.AbstractIdentifiableInitializableComponent;
+import net.shibboleth.utilities.java.support.component.ComponentInitializationException;
+import net.shibboleth.utilities.java.support.component.ComponentSupport;
+import net.shibboleth.utilities.java.support.logic.Constraint;
+import net.shibboleth.utilities.java.support.primitive.StringSupport;
+
+/**
+ * A function that computes a hash value for the authorization code via required configurable lookup function.
+ * The signature signing algorithm found in the {@link SecurityParametersContext} is used for calculating the hash.
+ * The lookup function for the security parameters context is required to be set.
+ * 
+ * @since 3.2.0
+ */
+public class DefaultComputeAuthorizationCodeHashFunction extends AbstractIdentifiableInitializableComponent
+    implements ContextDataLookupFunction<ProfileRequestContext, String> {
+
+    /** Class logger. */
+    @Nonnull private Logger log = LoggerFactory.getLogger(DefaultComputeAuthorizationCodeHashFunction.class);
+
+    /**
+     * Strategy used to locate the {@link SecurityParametersContext} to use for calculating the code hash.
+     */
+    @NonnullAfterInit
+    private Function<ProfileRequestContext, SecurityParametersContext> securityParametersLookupStrategy;
+
+    /** Strategy used to locate the raw authorization code value (as {@link String}) to use for calculating the hash. */
+    @NonnullAfterInit private Function<ProfileRequestContext, String> authorizationCodeLookupStrategy;
+
+    /**
+     * Set the strategy used to locate the {@link SecurityParametersContext} to use.
+     * 
+     * @param strategy lookup strategy
+     */
+    public void setSecurityParametersLookupStrategy(
+            @Nonnull final Function<ProfileRequestContext, SecurityParametersContext> strategy) {
+        ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+
+        securityParametersLookupStrategy =
+                Constraint.isNotNull(strategy, "SecurityParameterContext lookup strategy cannot be null");
+    }
+
+    /**
+     * Set the strategy used to locate the authorization code value.
+     * 
+     * @param strategy lookup strategy
+     */
+    public void setAuthorizationCodeLookupStrategy(@Nonnull final Function<ProfileRequestContext, String> strategy) {
+        ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+
+        authorizationCodeLookupStrategy =
+                Constraint.isNotNull(strategy, "AuthorizationCode lookup strategy cannot be null");
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    protected void doInitialize() throws ComponentInitializationException {
+        super.doInitialize();
+        
+        if (securityParametersLookupStrategy == null) {
+            throw new ComponentInitializationException("SecurityParameterContext lookup strategy cannot be null");
+        }
+        if (authorizationCodeLookupStrategy == null) {
+            throw new ComponentInitializationException("AuthorizationCode lookup strategy cannot be null");            
+        }
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    @Nullable
+    public String apply(@Nullable final ProfileRequestContext input) {
+        final String rawCode = authorizationCodeLookupStrategy.apply(input);
+        if (StringSupport.trimOrNull(rawCode) == null) {
+            log.error("Could not resolve a value for authorization code to calculate the hash value");
+            return null;
+        }
+        final AuthorizationCode code = new AuthorizationCode(rawCode);
+        final SecurityParametersContext securityParameters = securityParametersLookupStrategy.apply(input);
+        if (securityParameters == null || securityParameters.getSignatureSigningParameters() == null) {
+            log.error("Could not resolve security parameters for calculating the code hash value");
+            return null;
+        }
+        final SignatureSigningParameters signingParameters = securityParameters.getSignatureSigningParameters();
+        final CodeHash cHash = CodeHash.compute(code, new JWSAlgorithm(signingParameters.getSignatureAlgorithm()),
+                null);
+        if (cHash == null || cHash.getValue() == null) {
+            log.error("Not able to generate c_hash using algorithm {}", signingParameters.getSignatureAlgorithm());
+            return null;
+        }
+        return cHash.getValue();
+    }
+}
diff --git a/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/context/navigate/ResponseContextAuthorizationCodeLookupFunction.java b/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/context/navigate/ResponseContextAuthorizationCodeLookupFunction.java
new file mode 100644
index 00000000..ef7a6cd0
--- /dev/null
+++ b/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/context/navigate/ResponseContextAuthorizationCodeLookupFunction.java
@@ -0,0 +1,50 @@
+/*
+ * 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.oidc.op.profile.context.navigate;
+
+import javax.annotation.Nullable;
+
+import org.opensaml.messaging.context.navigate.ContextDataLookupFunction;
+import org.opensaml.profile.context.ProfileRequestContext;
+
+import net.shibboleth.idp.plugin.oidc.op.messaging.context.OIDCAuthenticationResponseContext;
+
+/**
+ * A function that returns raw authorization code value from the {@link OIDCAuthenticationResponseContext}. Null is
+ * returned if the function is not able to locate a value.
+ *
+ * @since 3.2.0
+ */
+public class ResponseContextAuthorizationCodeLookupFunction implements
+    ContextDataLookupFunction<ProfileRequestContext, String> {
+
+    /** {@inheritDoc} */
+    @Nullable
+    public String apply(@Nullable final ProfileRequestContext input) {
+        if (input == null || input.getOutboundMessageContext() == null) {
+            return null;
+        }
+        final OIDCAuthenticationResponseContext ctx =
+                input.getOutboundMessageContext().getSubcontext(OIDCAuthenticationResponseContext.class);
+        if (ctx == null || ctx.getAuthorizationCode() == null) {
+            return null;
+        }
+        return ctx.getAuthorizationCode().getValue();
+    }
+
+}
\ No newline at end of file
diff --git a/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/context/navigate/TokenRequestAuthorizationCodeLookupFunction.java b/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/context/navigate/TokenRequestAuthorizationCodeLookupFunction.java
new file mode 100644
index 00000000..3a7b4125
--- /dev/null
+++ b/idp-oidc-extension-api/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/context/navigate/TokenRequestAuthorizationCodeLookupFunction.java
@@ -0,0 +1,44 @@
+/*
+ * 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.oidc.op.profile.context.navigate;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import com.nimbusds.oauth2.sdk.AuthorizationCodeGrant;
+import com.nimbusds.oauth2.sdk.GrantType;
+import com.nimbusds.oauth2.sdk.TokenRequest;
+
+/**
+ * A function that returns raw authorization code value via a lookup function. This lookup locates authorization code
+ * from token claims set for token request handling. If token claims are not available, null is returned.
+ *
+ * @since 3.2.0
+ */
+public class TokenRequestAuthorizationCodeLookupFunction extends AbstractTokenRequestLookupFunction<String> {
+
+    /** {@inheritDoc} */
+    @Override @Nullable String doLookup(@Nonnull final TokenRequest req) {
+        if (GrantType.AUTHORIZATION_CODE.equals(req.getAuthorizationGrant().getType())) {
+            final AuthorizationCodeGrant grant = (AuthorizationCodeGrant) req.getAuthorizationGrant();
+            return grant.getAuthorizationCode().getValue();
+        }
+        return null;
+    }
+
+}
\ No newline at end of file
diff --git a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/AddAuthorizationCodeHashToIDToken.java b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/AddAuthorizationCodeHashToIDToken.java
index 6a7760ec..7c81b325 100644
--- a/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/AddAuthorizationCodeHashToIDToken.java
+++ b/idp-oidc-extension-impl/src/main/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/AddAuthorizationCodeHashToIDToken.java
@@ -17,19 +17,25 @@
 
 package net.shibboleth.idp.plugin.oidc.op.profile.impl;
 
+import java.util.function.Function;
+
 import javax.annotation.Nonnull;
 import org.opensaml.profile.action.ActionSupport;
 import org.opensaml.profile.action.EventIds;
 import org.opensaml.profile.context.ProfileRequestContext;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
-import com.nimbusds.jose.JWSAlgorithm;
-import com.nimbusds.openid.connect.sdk.claims.CodeHash;
 import com.nimbusds.openid.connect.sdk.claims.IDTokenClaimsSet;
 
+import net.shibboleth.utilities.java.support.annotation.constraint.NonnullAfterInit;
+import net.shibboleth.utilities.java.support.component.ComponentInitializationException;
+import net.shibboleth.utilities.java.support.component.ComponentSupport;
+import net.shibboleth.utilities.java.support.logic.Constraint;
+import net.shibboleth.utilities.java.support.primitive.StringSupport;
+
 /**
- * Action that adds authorization code hash claim to a {@link IDTokenClaimsSet}. If there are no signing parameters
- * available, action fails without error event.
+ * Action that adds authorization code hash claim to a {@link IDTokenClaimsSet}. Configurable calculation strategy
+ * function is used for producing the hash.
  */
 public class AddAuthorizationCodeHashToIDToken extends AbstractOIDCSigningResponseAction {
 
@@ -37,6 +43,31 @@ public class AddAuthorizationCodeHashToIDToken extends AbstractOIDCSigningRespon
     @Nonnull
     private Logger log = LoggerFactory.getLogger(AddAuthorizationCodeHashToIDToken.class);
 
+    /** The strategy used for calculating the authorization code hash value. */
+    @NonnullAfterInit private Function<ProfileRequestContext, String> codeHashCalculationStrategy;
+
+    /**
+     * Set the strategy used for calculating the authorization code hash value.
+     * 
+     * @param strategy calculation strategy
+     */
+    public void setCodeHashCalculationStrategy(@Nonnull final Function<ProfileRequestContext, String> strategy) {
+        ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+
+        codeHashCalculationStrategy =
+                Constraint.isNotNull(strategy, "Authorization code hash calculation strategy cannot be null");
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    protected void doInitialize() throws ComponentInitializationException {
+        super.doInitialize();
+
+        if (codeHashCalculationStrategy == null) {
+            throw new ComponentInitializationException("Authorization code hash calculation strategy cannot be null");
+        }
+    }
+
     /** {@inheritDoc} */
     @Override
     protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
@@ -46,21 +77,14 @@ public class AddAuthorizationCodeHashToIDToken extends AbstractOIDCSigningRespon
             ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_MSG_CTX);
             return;
         }
-        if (getOidcResponseContext().getAuthorizationCode() == null) {
-            log.error("{} No authz code to calculate hash on", getLogPrefix());
-            ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_MSG_CTX);
-            return;
-        }
-        final CodeHash cHash = CodeHash.compute(getOidcResponseContext().getAuthorizationCode(),
-                new JWSAlgorithm(getSignatureSigningParameters().getSignatureAlgorithm()));
-        if (cHash == null || cHash.getValue() == null) {
-            log.error("{} Not able to generate c_hash using algorithm {}", getLogPrefix(),
-                    getSignatureSigningParameters().getSignatureAlgorithm());
+        final String hashValue = codeHashCalculationStrategy.apply(profileRequestContext);
+        if (StringSupport.trimOrNull(hashValue) == null) {
+            log.error("{} Could not produce the authorization code hash value", getLogPrefix());
             ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_SEC_CFG);
             return;
         }
         log.debug("{} Setting authz code hash to id token", getLogPrefix());
-        getOidcResponseContext().getIDToken().setClaim(IDTokenClaimsSet.C_HASH_CLAIM_NAME, cHash.getValue());
+        getOidcResponseContext().getIDToken().setClaim(IDTokenClaimsSet.C_HASH_CLAIM_NAME, hashValue);
         log.debug("{} Updated token {}", getLogPrefix(),
                 getOidcResponseContext().getIDToken().toJSONObject().toJSONString());
 
diff --git a/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/authorize/authorize-beans.xml b/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/authorize/authorize-beans.xml
index 787b1ad2..d2c929c0 100644
--- a/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/authorize/authorize-beans.xml
+++ b/idp-oidc-extension-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oidc/authorize/authorize-beans.xml
@@ -531,6 +531,18 @@
                 c:g-ref="shibboleth.ChildLookup.SecurityParameters"
                 c:f-ref="shibboleth.ChildLookup.RelyingParty" />
         </property>
+        <property name="codeHashCalculationStrategy">
+            <bean class="net.shibboleth.idp.plugin.oidc.op.profile.context.navigate.DefaultComputeAuthorizationCodeHashFunction">
+                <property name="securityParametersLookupStrategy">
+                    <bean parent="shibboleth.Functions.Compose"
+                        c:g-ref="shibboleth.ChildLookup.SecurityParameters"
+                        c:f-ref="shibboleth.ChildLookup.RelyingParty" />
+                </property>
+                <property name="authorizationCodeLookupStrategy">
+                    <bean class="net.shibboleth.idp.plugin.oidc.op.profile.context.navigate.ResponseContextAuthorizationCodeLookupFunction" />
+                </property>
+            </bean>
+        </property>
     </bean>
 
     <bean id="ManipulateClaimsForIDToken"
diff --git a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/AddAuthorizationCodeHashToIDTokenTest.java b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/AddAuthorizationCodeHashToIDTokenTest.java
index c4981c6d..ca2b85ec 100644
--- a/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/AddAuthorizationCodeHashToIDTokenTest.java
+++ b/idp-oidc-extension-impl/src/test/java/net/shibboleth/idp/plugin/oidc/op/profile/impl/AddAuthorizationCodeHashToIDTokenTest.java
@@ -20,6 +20,8 @@ package net.shibboleth.idp.plugin.oidc.op.profile.impl;
 import java.net.URISyntaxException;
 import java.time.Instant;
 
+import net.shibboleth.idp.plugin.oidc.op.profile.context.navigate.DefaultComputeAuthorizationCodeHashFunction;
+import net.shibboleth.idp.plugin.oidc.op.profile.context.navigate.ResponseContextAuthorizationCodeLookupFunction;
 import net.shibboleth.idp.profile.testing.ActionTestingSupport;
 import net.shibboleth.utilities.java.support.component.ComponentInitializationException;
 
@@ -50,7 +52,12 @@ public class AddAuthorizationCodeHashToIDTokenTest extends BaseOIDCResponseActio
         profileRequestCtx.addSubcontext(spCtx);
         setIdTokenToResponseContext("iss", "sub", "aud", Instant.now(), Instant.now());
         respCtx.setAuthorizationCode("authcode");
+        final DefaultComputeAuthorizationCodeHashFunction hashFunction =
+                new DefaultComputeAuthorizationCodeHashFunction();
+        hashFunction.setAuthorizationCodeLookupStrategy(new ResponseContextAuthorizationCodeLookupFunction());
+        hashFunction.setSecurityParametersLookupStrategy(prc -> spCtx);
         action = new AddAuthorizationCodeHashToIDToken();
+        action.setCodeHashCalculationStrategy(hashFunction);
         action.initialize();
     }
 
@@ -77,11 +84,11 @@ public class AddAuthorizationCodeHashToIDTokenTest extends BaseOIDCResponseActio
      * @throws URISyntaxException
      */
     @Test
-    public void testAuthorizationCode() throws ComponentInitializationException, ParseException, URISyntaxException {
+    public void testNoAuthorizationCode() throws ComponentInitializationException, ParseException, URISyntaxException {
         init("RS256", credentialRSA);
         respCtx.setAuthorizationCode(null);
         final Event event = action.execute(requestCtx);
-        ActionTestingSupport.assertEvent(event, EventIds.INVALID_MSG_CTX);
+        ActionTestingSupport.assertEvent(event, EventIds.INVALID_SEC_CFG);
     }
 
     /**

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


More information about the commits mailing list