Ex: Re: Controlling IdP response status based on attribute

Paul B. Henson henson at cpp.edu
Wed Oct 16 19:57:25 UTC 2024


> From: Cantor, Scott
> Sent: Wednesday, October 16, 2024 8:01 AM
> 
> But it would be largely equivalent anyway, you'd have to signal a custom event
> using the context-check flow and then configure that event in errors.xml as a
> "non-local" event that would result in a response and add a mapping for the
> event to the SAML status (which offhand I think is possible but don't recall how
> to do, and probably isn't documented anywhere).

It's fairly simple to use the context check intercept to perform local idp-side authorization.

First, you need to update conf/intercept/context-check-intercept-config.xml and supply a condition, our condition is:

    <bean id="shibboleth.context-check.Condition"
          parent="shibboleth.Conditions.SimpleAttribute"
          p:useUnfilteredAttributes="true">
        <property name="attributeValueMap">
            <map>
                <!-- This attribute is synthetized in the attribute resolver config, as
                     that is dynamically reloadable and this is not. If the attribute is not
                     set to 1, deny access to service. -->
                <entry key="idp_authz">
                    <list>
                        <value>1</value>
                    </list>
                </entry>
            </map>
        </property>
    </bean>

Changes in this file require an idp restart, so rather than have any logic here, I place the logic in the attribute resolver which is dynamically reloadable and simply check the results here. Theoretically if there is one and only one SP you want to do this on and you configure the context check intercept only run for that SP you could hardcode the logic here. But I guarantee at some point you will run into another SP that requires idp-side authorization, particularly ones that are geared towards the commercial cloud services like Azure, Okta, Ping et al.

Our idp_authz is a scripted attribute which contains a list of services requiring authorization and checks either the memberOf attribute or our local cppEduPersonAffiliation attribute:

    <!-- Attribute used for idp side authz context check intercept. -->
    <!-- Returns 1 if client authorized for service or 0 if not.    -->
    <AttributeDefinition id="idp_authz"
                         xsi:type="ScriptedAttribute"
                         excludeResolutionPhases="c14n/attribute ResolverServiceClientSecretValueResolver">
        <InputDataConnector ref="LDAP" attributeNames="cppEduPersonAffiliation"/>
        <InputDataConnector ref="LDAP" attributeNames="memberOf"/>
        <InputDataConnector ref="LDAP" attributeNames="uid"/>
        <Script><![CDATA[
            "use strict";

            var logger = Java.type("org.slf4j.LoggerFactory").getLogger("idp_authz");
            var attr_type = Java.type("net.shibboleth.idp.attribute.IdPAttribute");
            var ArrayList = Java.type("java.util.ArrayList");

            // If user doesn't have necessary attributes to check,
            // create empty instances to avoid undefined variable errors
            var memberOf_attr;
            if (typeof memberOf != "undefined") {
                memberOf_attr = memberOf;
            }
            else {
                logger.debug("creating empty memberOf attribute");
                memberOf_attr = new attr_type("memberOf");
            }
            var cppEduPersonAffiliation_attr;
            if (typeof cppEduPersonAffiliation != "undefined") {
                cppEduPersonAffiliation_attr = cppEduPersonAffiliation;
            }
            else {
                logger.debug("creating empty cppEduPersonAffiliation attribute");
                cppEduPersonAffiliation_attr = new attr_type("cppEduPersonAffiliation");
            }
            var username;
            if (typeof uid != "undefined") {
                var uid_values = uid.getValues();
                if (uid_values.length > 0) {
                    username = uid_values[0];
                }
                else {
                    logger.debug("uid has no values");
                    username = "<none>";
                }
            }
            else {
                logger.debug("uid not defined");
                username = "<missing>";
            }

            // Authorization rules definition:
            //   Name of relying party needing client side check
            //     What attribute to look at
            //     Which values of attribute required to be authorized
            //
            var authz_rules = {
                'https://account-d.docusign.com/organizations/101b6702-f8fb-4249-a724-896a1155678d/saml2':
                    {
                        'attr': memberOf_attr,
                        'values': new ArrayList([ 'uid=docusign_user,ou=group,dc=cpp,dc=edu' ])
                    },
                'https://account.docusign.com/organizations/eca3f434-3540-49d0-94b5-fceb336832f7/saml2':
                    {
                        'attr': memberOf_attr,
                        'values': new ArrayList([ 'uid=docusign_user,ou=group,dc=cpp,dc=edu' ])
                    },
                'https://www.concursolutions.com':
                    {
                        'attr': cppEduPersonAffiliation_attr,
                        'values': new ArrayList([ 'employee', 'pending_faculty', 'past_faculty' ])
                    },
                'test-auth.onlinephotosubmission.com':
                    {
                        'attr': cppEduPersonAffiliation_attr,
                        'values': new ArrayList([ 'member', 'pending_faculty', 'pending_staff', 'affiliate' ])
                    },
                'auth.onlinephotosubmission.com':
                    {
                        'attr': cppEduPersonAffiliation_attr,
                        'values': new ArrayList([ 'member', 'pending_faculty', 'pending_staff', 'affiliate' ])
                    },
                'http://app.pantrysoft.com/saml/id':
                    {
                    'attr': memberOf_attr,
                    'values': new ArrayList([ 'uid=students,ou=group,dc=cpp,dc=edu' ])
                    },
                'urn:auth0:luxerone:caly-pomona':
                    {
                    'attr': memberOf_attr,
                    'values': new ArrayList([ 'uid=luxer_managers,ou=group,dc=cpp,dc=edu', 'uid=luxer_employees,ou=group,dc=cpp,dc=edu' ])
                    },
                                'https://sso.grammarly.com/saml/metadata':
                    {
                    'attr': memberOf_attr,
                    'values': new ArrayList([ 'uid=grammarly_users,ou=group,dc=cpp,dc=edu' ])
                    },
                'https://app.sharebase.com/sharebaseapi/idp':
                    {
                    'attr': memberOf_attr,
                    'values': new ArrayList([ 'uid=sharebase_users,ou=group,dc=cpp,dc=edu' ])
                    },
                'https://sso-staging.streemetv.com/sp/pomona':
                    {
                    'attr': memberOf_attr,
                    'values': new ArrayList([ 'uid=idm_admin-sys,ou=group,dc=cpp,dc=edu',
                                                                                                'uid=uhs_residentsupd,ou=group,dc=cpp,dc=edu' ])
                    },
                'https://sso.streemetv.com/sp/pomona':
                    {
                    'attr': memberOf_attr,
                    'values': new ArrayList([ 'uid=idm_admin-sys,ou=group,dc=cpp,dc=edu',
                                                                                                'uid=uhs_residents,ou=group,dc=cpp,dc=edu',
                                                                                                'uid=uhs_residentsupd,ou=group,dc=cpp,dc=edu' ])
                    }
            };

            var value = '1';

            var rpCtx = profileContext.getSubcontext("net.shibboleth.profile.context.RelyingPartyContext");

            if (rpCtx != null) {
                var rp = rpCtx.getRelyingPartyId();
                logger.debug("checking rp " + rp);

                if (rp in authz_rules) {
                    value = '0';
                    var attr = authz_rules[rp]['attr'];
                    var values = authz_rules[rp]['values'];
                    logger.debug("requires attr " + attr.getId() + " matching " + values);
                    var attr_values = attr.getValues();
                    logger.debug("user " + username + " attr values " + attr_values);

                    for (var i = 0; i < attr_values.length; i++) {
                        if (values.contains(attr_values[i])) {
                            logger.debug("found " + attr_values[i]);
                            value = '1';
                            break;
                        }
                    }

                    if (value.equals('0')) {
                        logger.info("user " + username + " for service " + rp + " failed - " +
                                    "required " + attr.getId() + " matching " + values + "; " +
                                    " found " + attr_values);
                    }
                } else {
                    logger.debug("no idp authz required");
                }
            } else {
                logger.error('RelyingPartyContext null, failing open');
            }

            logger.debug("returning " + value);
            idp_authz.addValue(value);
        ]]></Script>
    </AttributeDefinition>

As you can see, we have a fair number of these services. Next, you need to ensure the context check intercept runs either in general or for the specific set of SP's you want it to run for. In our case, we just run it for all services by creating a custom bean:

    <!-- Custom config for default configuration -->
    <bean id="SAML2.SSO.custom" parent="SAML2.SSO.MDDriven"
        p:postAuthenticationFlows="#{{'context-check', 'warning'}}"
    />

and using that bean in the default relying party config:

                <ref bean="SAML2.SSO.custom" />

Then you need to update conf/errors.xml to define a view for when the check fails:

    <!-- Map local events to alternate view templates. -->
    <util:map id="shibboleth.EventViewMap">
        <entry key="ContextCheckDenied" value="authz_failed" />
    </util:map>

Finally need to create the view as views/authz_failed.vm

I believe I covered all of the necessary pieces... I think this is a better approach than returning a failed result to an SP which will give an end-user some generic oh no something is horribly broken error 8-/. Our view informs the user they don't have access to this particular application and either provides application-specific details as to what is required and who to contact to acquire access or a general recommendation to contact our service desk for assistance.



More information about the users mailing list