[java-idp-plugin-oidc-rp] branch main updated: Add UserInfo JWT claim validation

Phil Smart philip.smart at jisc.ac.uk
Wed Jun 8 11:11:03 UTC 2022


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=8cfdad4b8d168be9deed1427c6640074a022da00

The following commit(s) were added to refs/heads/main by this push:
     new 8cfdad4  Add UserInfo JWT claim validation
8cfdad4 is described below

commit 8cfdad4b8d168be9deed1427c6640074a022da00
Author: Phil Smart <philip.smart at jisc.ac.uk>
AuthorDate: Wed Jun 8 12:10:58 2022 +0100

    Add UserInfo JWT claim validation
---
 .../navigate/SubFromIDTokenLookupFunction.java     | 95 ++++++++++++++++++++++
 .../authn/oidc/rp/messaging/UserInfoResponse.java  |  2 +-
 .../impl/DefaultUserInfoTokenLookupStrategy.java   |  3 +-
 .../authn/oidc/rp/impl/ValidateTokenClaims.java    |  6 +-
 .../oidc-relying-party-authn-beans.xml             | 63 +++++++++-----
 .../oidc-relying-party-authn-flow.xml              |  9 +-
 .../plugin/authn/oidc/rp/impl/OIDCRPFlowTest.java  |  2 +-
 7 files changed, 148 insertions(+), 32 deletions(-)

diff --git a/idp-oidc-rp-api/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/config/navigate/SubFromIDTokenLookupFunction.java b/idp-oidc-rp-api/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/config/navigate/SubFromIDTokenLookupFunction.java
new file mode 100644
index 0000000..5b4147c
--- /dev/null
+++ b/idp-oidc-rp-api/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/config/navigate/SubFromIDTokenLookupFunction.java
@@ -0,0 +1,95 @@
+/*
+ * 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.config.navigate;
+
+import java.text.ParseException;
+import java.util.function.BiFunction;
+import java.util.function.Function;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.ThreadSafe;
+
+import org.opensaml.messaging.context.navigate.ChildContextLookup;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.opensaml.profile.context.navigate.InboundMessageContextLookup;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.nimbusds.jwt.JWTClaimsSet;
+
+import net.shibboleth.idp.plugin.authn.oidc.rp.context.AccessTokenResponseContext;
+import net.shibboleth.utilities.java.support.logic.Constraint;
+
+/**
+ * A function that pulls the subject 'sub' out of the id_token in the {@link AccessTokenResponseContext}.
+ */
+ at ThreadSafe
+public class SubFromIDTokenLookupFunction implements BiFunction<ProfileRequestContext, JWTClaimsSet, String> {
+
+    /** Class logger. */
+    @Nonnull
+    private final Logger log = LoggerFactory.getLogger(SubFromIDTokenLookupFunction.class);
+
+    /** Strategy used to locate the {@link AccessTokenResponseContext} to extract the id_token from. */
+    @Nonnull
+    private final Function<ProfileRequestContext, AccessTokenResponseContext> tokenResponseContextLookupStrategy;
+
+    /** Constructor. */
+    public SubFromIDTokenLookupFunction() {
+        tokenResponseContextLookupStrategy = new ChildContextLookup<>(AccessTokenResponseContext.class, true)
+                .compose(new InboundMessageContextLookup());
+    }
+
+    /**
+     * 
+     * Constructor.
+     *
+     * @param strategy the AccessTokenResponseContext lookup strategy to use.
+     */
+    public SubFromIDTokenLookupFunction(
+            @Nonnull final Function<ProfileRequestContext, AccessTokenResponseContext> strategy) {
+        tokenResponseContextLookupStrategy =
+                Constraint.isNotNull(strategy, "AccessTokenResponseContext lookup strategy can not be null");
+    }
+
+    /**
+     * {@inheritDoc}
+     * 
+     * The input claims set is ignored because it belongs to the JWT that is being tested. Here we lookup
+     * the 'sub' claim from the id_token in a different context.
+     */
+    @Override
+    @Nullable
+    public String apply(@Nonnull final ProfileRequestContext prc, @Nullable final JWTClaimsSet claimsSet) {
+        final AccessTokenResponseContext tokenContext = tokenResponseContextLookupStrategy.apply(prc);
+        if (tokenContext == null || tokenContext.getIdToken() == null) {
+            return null;
+        }
+        try {
+            final JWTClaimsSet claims = tokenContext.getIdToken().getJWTClaimsSet();
+            if (claims.getSubject() != null) {
+                return claims.getSubject();
+            }
+        } catch (final ParseException e) {
+            log.warn("Unable to parse id_token claims, can not extract subjuect", e);
+        }
+        return null;
+    }
+
+}
diff --git a/idp-oidc-rp-api/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/messaging/UserInfoResponse.java b/idp-oidc-rp-api/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/messaging/UserInfoResponse.java
index 9cb9927..4a39ca9 100644
--- a/idp-oidc-rp-api/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/messaging/UserInfoResponse.java
+++ b/idp-oidc-rp-api/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/messaging/UserInfoResponse.java
@@ -28,7 +28,7 @@ import com.nimbusds.openid.connect.sdk.claims.ClaimsSet;
 public interface UserInfoResponse {
     
     /** Which type of response is this.*/
-    enum UserInfoResponseType  {
+    public enum UserInfoResponseType  {
         /** A plain JSON Object type.*/
         PLAIN,
         
diff --git a/idp-oidc-rp-impl/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/impl/DefaultUserInfoTokenLookupStrategy.java b/idp-oidc-rp-impl/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/impl/DefaultUserInfoTokenLookupStrategy.java
index 9910f35..f60abf3 100644
--- a/idp-oidc-rp-impl/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/impl/DefaultUserInfoTokenLookupStrategy.java
+++ b/idp-oidc-rp-impl/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/impl/DefaultUserInfoTokenLookupStrategy.java
@@ -32,6 +32,7 @@ import com.nimbusds.jwt.JWT;
 import net.shibboleth.idp.plugin.authn.oidc.rp.context.AccessTokenResponseContext;
 import net.shibboleth.idp.plugin.authn.oidc.rp.context.UserInfoResponseContext;
 import net.shibboleth.idp.plugin.authn.oidc.rp.messaging.JWTUserInfoResponse;
+import net.shibboleth.idp.plugin.authn.oidc.rp.messaging.UserInfoResponse.UserInfoResponseType;
 import net.shibboleth.utilities.java.support.logic.Constraint;
 
 /** Function that extracts the id_token from the {@link AccessTokenResponseContext}.*/
@@ -69,7 +70,7 @@ public class DefaultUserInfoTokenLookupStrategy implements Function<ProfileReque
         final UserInfoResponseContext userInfoContext = userInfoResponseContextLookupStrategy.apply(prc);
         
         if (userInfoContext == null || userInfoContext.getUserInfo() == null ||
-                !(userInfoContext.getUserInfo() instanceof JWTUserInfoResponse)) {
+                userInfoContext.getUserInfo().getType() != UserInfoResponseType.JWT) {
             return null;
         }
         return ((JWTUserInfoResponse)userInfoContext.getUserInfo()).getResponseJwt();
diff --git a/idp-oidc-rp-impl/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/impl/ValidateTokenClaims.java b/idp-oidc-rp-impl/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/impl/ValidateTokenClaims.java
index b1a2bb9..94b7ad9 100644
--- a/idp-oidc-rp-impl/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/impl/ValidateTokenClaims.java
+++ b/idp-oidc-rp-impl/src/main/java/net/shibboleth/idp/plugin/authn/oidc/rp/impl/ValidateTokenClaims.java
@@ -173,7 +173,7 @@ public class ValidateTokenClaims extends AbstractOIDCAuthenticationAction {
     protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext,
             @Nonnull final AuthenticationContext authenticationContext) {
 
-        log.debug("{} Validating JWT token claims for subject '{}'",getLogPrefix(),
+        log.debug("{} Validating JWT claims for subject '{}'",getLogPrefix(),
                 claimsSet.getSubject() != null ? claimsSet.getSubject() : "unknown subject");
          
         try {
@@ -182,7 +182,7 @@ public class ValidateTokenClaims extends AbstractOIDCAuthenticationAction {
                 cleanupHook.accept(profileRequestContext);
             }
         } catch (final JWTValidationException e) {
-            log.error("{} JWT token verification failed for subject '{}'", getLogPrefix(),claimsSet.getSubject(),e);
+            log.error("{} JWT verification failed for subject '{}'", getLogPrefix(),claimsSet.getSubject(),e);
             ActionSupport.buildEvent(profileRequestContext, AuthnEventIds.NO_CREDENTIALS);
             if (cleanupHook != null) {
                 cleanupHook.accept(profileRequestContext);
@@ -190,7 +190,7 @@ public class ValidateTokenClaims extends AbstractOIDCAuthenticationAction {
             return;
         }
         //fine.
-        log.debug("{} JWT token claims are valid for subject '{}'",getLogPrefix(),claimsSet.getSubject());
+        log.debug("{} JWT claims are valid for subject '{}'",getLogPrefix(),claimsSet.getSubject());
     }
     
 
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 7c95664..26d175a 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
@@ -364,7 +364,7 @@
     <bean id="shibboleth.SignatureValidationParametersResolver"
         class="org.opensaml.xmlsec.impl.BasicSignatureValidationParametersResolver" />
 
-    <!-- Default id_token JWT validation wiring. -->
+    <!-- Default id_token and some UserInfo JWT validation wiring. -->
 
     <!-- No default cleanup, maybe could be to remove nonce etc. -->
     <bean id="ValidateIDTokenClaims" parent="NestedWebFlowProfileActionAdaptor" scope="prototype"
@@ -583,27 +583,10 @@
             </bean>
         </constructor-arg>
     </bean>
+    
+     <!-- UserInfo response JWT validation -->
 
-
-    <!-- UserInfo Decryption and Signature Validation Done -->
-
-
-    <bean id="ValidateUserInfoClaims" parent="NestedWebFlowProfileActionAdaptor" scope="prototype"
-        class="net.shibboleth.idp.plugin.authn.oidc.rp.impl.ValidateUserInfoClaims"
-        p:authenticationContextLookupStrategy-ref="ParentAuthenticiationContextLookup" />
-
-
-    <bean id="ProcessEndUserClaims" class="net.shibboleth.idp.plugin.authn.oidc.rp.impl.ProcessEndUserClaims"
-        scope="prototype" p:profileContextLookupStrategy-ref="shibboleth.ChildLookup.ProxyProfileRequestContext"
-        p:authenticationContextLookupStrategy-ref="ParentAuthenticiationContextLookup" />
-
-
-    <bean id="CheckUserInfoPlainResponseTypeCondition"
-        class="net.shibboleth.idp.plugin.authn.oidc.rp.messaging.context.logic.UserInfoPlainResponseTypeCondition" />
-
-    <!-- UserInfo response JWT validation -->
-
-    <bean id="ValidateUserInfoToken" parent="NestedWebFlowProfileActionAdaptor" scope="prototype"
+    <bean id="ValidateUserInfoTokenClaims" parent="NestedWebFlowProfileActionAdaptor" scope="prototype"
         class="net.shibboleth.idp.plugin.authn.oidc.rp.impl.ValidateTokenClaims"
         p:authenticationContextLookupStrategy-ref="ParentAuthenticiationContextLookup"
         p:cleanupHook="#{getObject('shibboleth.authn.oidc.rp.userinfo.jwt.claims.CleanUpHook')}"
@@ -620,12 +603,48 @@
         class="net.shibboleth.idp.plugin.authn.oidc.rp.impl.DefaultUserInfoTokenLookupStrategy" />
 
     <util:list id="UserInfoClaimsValidators" value-type="net.shibboleth.oidc.jwt.claims.ClaimsValidator">
+        <ref bean="SubClaimRequiredValidator"/>
+        <ref bean="SubMatchesIDTokenClaimValidator"/>
         <ref bean="IssuerClaimsValidator" />
         <ref bean="AudienceClaimsValidator" />
-    </util:list>
+    </util:list>    
+    
+    <bean id="SubClaimRequiredValidator"
+        class="net.shibboleth.oidc.security.jwt.claims.impl.RequiredClaimsValidator">
+        <property name="requiredClaims">
+            <list>
+                <value>sub</value>
+            </list>
+        </property>
+    </bean>
+    
+    <bean id="SubMatchesIDTokenClaimValidator" 
+        class="net.shibboleth.oidc.security.jwt.claims.impl.ExactMatchClaimsValidator"
+        p:claimName="sub">
+        <property name="valueToMatchLookupStrategy">
+            <bean class="net.shibboleth.idp.plugin.authn.oidc.rp.config.navigate.SubFromIDTokenLookupFunction"/>
+        </property>
+     </bean>
+        
+    
+
+    <!-- UserInfo Decryption and Signature Validation Done -->
+
+    <!-- TODO turn this into a ClaimsValidator actions -->
+    <bean id="ValidateUserInfoPlainResponseClaims" parent="NestedWebFlowProfileActionAdaptor" scope="prototype"
+        class="net.shibboleth.idp.plugin.authn.oidc.rp.impl.ValidateUserInfoClaims"
+        p:authenticationContextLookupStrategy-ref="ParentAuthenticiationContextLookup" />
 
 
+    <bean id="ProcessEndUserClaims" class="net.shibboleth.idp.plugin.authn.oidc.rp.impl.ProcessEndUserClaims"
+        scope="prototype" p:profileContextLookupStrategy-ref="shibboleth.ChildLookup.ProxyProfileRequestContext"
+        p:authenticationContextLookupStrategy-ref="ParentAuthenticiationContextLookup" />
+
+
+    <bean id="CheckUserInfoPlainResponseTypeCondition"
+        class="net.shibboleth.idp.plugin.authn.oidc.rp.messaging.context.logic.UserInfoPlainResponseTypeCondition" />
 
+   
     <!-- Final validation and proxy authentication result -->
 
     <bean id="ValidateOIDCAuthentication" parent="NestedWebFlowProfileActionAdaptor"
diff --git a/idp-oidc-rp-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/authn/OIDCRelyingParty/oidc-relying-party-authn-flow.xml b/idp-oidc-rp-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/authn/OIDCRelyingParty/oidc-relying-party-authn-flow.xml
index 911b391..807aa84 100644
--- a/idp-oidc-rp-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/authn/OIDCRelyingParty/oidc-relying-party-authn-flow.xml
+++ b/idp-oidc-rp-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/authn/OIDCRelyingParty/oidc-relying-party-authn-flow.xml
@@ -111,8 +111,8 @@
         <evaluate expression="PopulateIDTokenDecryptionParameters" />
         <evaluate expression="DecryptJWT"/>
         <evaluate expression="PopulateIDTokenSignatureValidationParameters" />
+        <!-- TODO Not sure if HandleIDTokenValidation needs to be a message chain?  -->
         <evaluate expression="HandleIDTokenValidation" />
-
         <evaluate expression="ValidateIDTokenClaims" />
         <evaluate expression="'proceed'" />
         <transition on="proceed" to="CheckUserInfoClaimsRequired" />
@@ -143,13 +143,14 @@
         <evaluate expression="DecryptUserInfoJWT"/>
         <evaluate expression="PopulateUserInfoTokenSignatureValidationParameters" />
         <evaluate expression="HandleUserInfoTokenValidation" />    
+        <evaluate expression="ValidateUserInfoTokenClaims" />      
         <evaluate expression="'proceed'" />
-        <transition on="proceed" to="ValidateUserInfoClaimsSet" />    
+        <transition on="proceed" to="FinalizeResponse" />    
     </action-state>
     
     <!-- Plain UserInfo response types will skip straight to this stage -->
-    <action-state id="ValidateUserInfoClaimsSet">
-        <evaluate expression="ValidateUserInfoClaims" />
+    <action-state id="ValidateUserInfoPlaimClaimsSet">
+        <evaluate expression="ValidateUserInfoPlainResponseClaims" />
         <evaluate expression="'proceed'" />
         <transition on="proceed" to="FinalizeResponse" />
     </action-state>
diff --git a/idp-oidc-rp-impl/src/test/java/net/shibboleth/idp/plugin/authn/oidc/rp/impl/OIDCRPFlowTest.java b/idp-oidc-rp-impl/src/test/java/net/shibboleth/idp/plugin/authn/oidc/rp/impl/OIDCRPFlowTest.java
index 346fdc8..525aae7 100644
--- a/idp-oidc-rp-impl/src/test/java/net/shibboleth/idp/plugin/authn/oidc/rp/impl/OIDCRPFlowTest.java
+++ b/idp-oidc-rp-impl/src/test/java/net/shibboleth/idp/plugin/authn/oidc/rp/impl/OIDCRPFlowTest.java
@@ -884,7 +884,7 @@ public class OIDCRPFlowTest extends AbstractAuthnXmlFlowExecutionTests {
     }
     
     @Test 
-    public void testAuthnFlowFromAuthorizationCallback_Using_AsymetricSignedAnEncrypted_IDTokenAndUserInfoResponse() 
+    public void testAuthnFlowFromAuthorizationCallback_Using_AsymetricSignedAndEncrypted_IDTokenAndUserInfoResponse() 
             throws Exception {
         
         setFlowPath(FLOW);

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


More information about the commits mailing list