A French version of this document can be found on the CANARIE website.

Please read and follow the documentation first, before or along with using this example. This documentation is not maintained by the development team and may not be entirely accurate or consistent with the software at any given time. It is a complement to the documentation, not a replacement for it. It is currently out of date with respect to some improvements made in V4.1.


Overview 

This configuration recipe of the Shibboleth IdP leverages the SAML proxying features such that Azure AD can be used for sign-in while Shibboleth handles the features many R&E federation's trust models value. Using this approach uses just the IdP as a SAML proxy without the necessity of extra elements.

We will cover two main proxying approaches :

The steps can be applied to any IdP installation and assume that a v4.0.1 IdP platform has been installed.  The Internet2 Trusted Access Platform IdP container was used during configuration testing and helped take care of some of the heavy lifting during installation (ie, the webserver, version of java, and externalizing configuration). 

Expected Audience

The configuration steps assume some familiarity with the Shibboleth IdP, its layout, and being familiar with software installation and sustainment. The steps are platform neutral and should work with the Windows or Unix version of the IdP.

Where to Install

This configuration can be applied to an IdP on-premises (on-prem) or in the cloud. The location of the IdP may be influenced by the location of your data. Unless all your data is kept in Azure AD you will likely be implementing a hybrid approach for attributes from both Azure AD and normal Shibboleth IdP attribute resolution.


Note: This new SAML proxying feature speaks to the evolving needs of the R&E community with cloud services that do not meet all R&E needs natively. This approach attempts to strike a balance between the vendor solution space and R&E requirements to interoperate with the R&E community.

Acknowledgements: Thanks to contributors  Former user (Deleted)the Shibboleth team, and others for guidance and input on the material.

IdP Proxying: Appearances and Perspectives

Proxying techniques are confusing at times and it is helpful to understand the approach and your perspective:

Azure ADs:

Relying Party (Service Provider) from the multi-lateral trust/R&E federation community:

Users: 

Federation Operator:

IdP Proxying: What a Proxy Flow Looks Like

When the user initiates sign-on the following usually happens:

Implementing the Solution

Prerequisites

Be sure to have these assets in place and appropriate access to them before you start:

  1. A working Shibboleth Identity Provider at V4 or above running somewhere.
  2. An active Azure AD tenant that you have administrative control in
  3. The ability in Azure AD to create an enterprise Non-Gallery SAML App
  4. A suitable attribute available to both IdPs to use as a "joining" attribute used in local lookups if necessary.

Terms and Settings 

This document refers to some key settings and terms described here. For your installation have these on hand to speed up configuration:

Steps and Tasks

Step 1. Configuring Trust Between Azure AD and the Shibboleth IdP

There are 3 main trust-related configurations to adjust:

3. the Azure AD trust to the Shibboleth IdP as a Relying Party

Trust Task: 1. Update your IdP's metadata

As your IdP will need act as an SP, you'll need extra blocks in your entity's metadata.

This SPSSODescriptor enriched metadata is used with your upstream IDP (Azure AD), not in your R&E federation and not necessary to be published in the R&E fed.


<EntityDescriptor entityID="https://idp.example.com/idp" ...>

    <!-- Already present IdP data -->
    <IDPSSODescriptor ...>
        ...
    </IDPSSODescriptor>

    <AttributeAuthorityDescriptor>
        ...
    </AttributeAuthorityDescriptor>


    <!-- New SP block -->
    <SPSSODescriptor protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol">

        <KeyDescriptor use="signing">
            <ds:KeyInfo>
                <ds:X509Data>
                    <ds:X509Certificate>
                    ...Signing Certificate from IdP...
                    </ds:X509Certificate>
                </ds:X509Data>
            </ds:KeyInfo>
        </KeyDescriptor>
        <KeyDescriptor use="encryption">
            <ds:KeyInfo>
                <ds:X509Data>
                    <ds:X509Certificate>
                    ...Encryption Certificate from IdP...
                    </ds:X509Certificate>
                </ds:X509Data>
            </ds:KeyInfo>
        </KeyDescriptor>

    <AssertionConsumerService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://idp.example.com/idp/profile/Authn/SAML2/POST/SSO" index="0"/>
</SPSSODescriptor>

</EntityDescriptor>

Trust Task: 2. Register your IdP with Azure AD

An Azure AD Enterprise Application needs to be created of type 'Non-Gallery Application' and configured for SAML.

Steps to create the app:

Once created edit the Basic SAML Configuration Section where you will use two things:

Other items like Logout URL can also be provided but the Shibboleth side will not handle logout in this case, so this is best omitted.


Keep this Relying Party editing environment open for the next step.

Trust Task: 3. Register the upstream IdP's metadata locally

Your Shibboleth IdP doesn't know of the Azure AD IdP so you need to register it locally. This is handled just like any other metadata and can be included in your metadata-providers.xml as a new <MetadataProvider> element. For example:

<MetadataProvider id="AzureAD-idp-metadata"
        xsi:type="FilesystemMetadataProvider"
        metadataFile="/opt/idp/metadata/AzureAD-idp.xml" />

The file AzureAD-idp.xml must contain valid SAML 2. Metadata for your tenant's entity containing:

This can be most easily found and downloaded by clicking on 'Federation Metadata XML Download' in Section 3 of the Azure AD Relying Party information, however it needs to be scrubbed a bit to produce a lean version of the metadata.

Hint: Those on mac or unix may find it convenient processing the file through xmllint --format downloadedfile.xml > downloadedfile-formated.xml
(xmllint
can be found in a package named libxml2 on RedHat or libxml2-utils on Debian/Ubuntu.)

Hand-adjust the downloaded metadata to add both the XML NameSpace and a value for shibmd:Scope in the IDPSSODescriptor element:

  1. Add to the EntityDescriptor in your downloaded metadata: xmlns:shibmd="urn:mace:shibboleth:metadata:1.0
  2. add the Extensions element inside the IDPSSODescriptor element to align with the scope validation per the rule in Proxy Task 2 below:
<Extensions>
  <shibmd:Scope regexp="false">your_scope_domain_here</shibmd:Scope>
</Extensions>

When done, add an entry to metadata-providers.xml.  If you fetch this entry elsewhere or by other means (e.g. curl or otherwise)  ensure you know and understand its pedigree and don't blindly trust it without some diligence of some sort.


Note!

  • About certificates:
    • The certificate generated by Azure AD is only 3 years in duration and is the trust between Azure and the Shibboleth Proxy only.
    • Expect Azure AD to cease trust when it expires.
    • Also expect certificate rollover between Azure AD and your IdP instance to be handled by the IdP admin team. 
  • About the metadata download  itself:
    • There are no linefeeds between lines (so reformat e.g. with xmllint as shown above, if desired)
    • It contains unnecessary elements that can be removed to reduce the size of the file:
      • a signature embedded in the top of the file
      • The section Fed:ClaimTypesOffered 

Trust Task: 4. Configure Azure AD Attribute Release to the Shibboleth IdP

While still in the edit mode of the Relying Party, configure the necessary attributes needed for the Shibboleth IdP operation.


user.userprincipalname is the usual choice for the main user identifier but review your practices carefully around its use and potential re-use. This is going to be especially critical around MFA and assurance attestations to ensure they have a unique identifier that is unchanging.

Step 2. Configure the IdP for Proxing Behaviour

The following changes are needed to adjust the IdP to proxying behaviour:

  1. Changing the authentication flow to SAML authentication
  2. Configuring which Entity to delegate to for the flow
  3. Update attribute filter to allow incoming attributes to be ingested
  4. Set up attribute extraction through Subject Canonicalisation (c14n and resolver)

Proxy Task 1. Change the IdP authentication flow to SAML

There are three steps for this task:

  1. Edit authn/saml-authn-config.xml to set the EntityID of the Azure AD Entity:
    1. Uncomment the shibboleth.authn.SAML.discoveryFunction bean and edit the target:
    2. REPLACE sts_tenant_id with your identifier. NOTE THE TRAILING SLASH IS TO BE INCLUDED
<bean id="shibboleth.authn.SAML.discoveryFunction" parent="shibboleth.Functions.Constant"
    c:target="https://sts.windows.net/<sts_tenant_id>/" />

2. Enable the flow by updating idp.authn.flows in idp.properties to set it to SAML:

idp.authn.flows=SAML

Proxy Task 2. Update Your Attribute Filter

The IdP will not ingest attributes from the Azure AD Upstream IdP unless they're allowed in by a filter.

  1. Add a new <AttributeFilterPolicy> to permit trusted attributes to be handled.
  2. REPLACE sts_tenant_id with your identifier. NOTE THE TRAILING SLASH TO BE INCLUDED

Note:  There is a policy rule ScopeMatchesShibMDScope in effect on the attribute 'name' (known internally as azureName).

This will enforce that the idp.scope set in conf/idp.properties agrees with the Azure AD field to avoid any oddities. If they disagree, this rule will fail-closed and not sign a user.

Be sure the IdP metadata for the your Azure AD IdP has the right scope for things to work smoothly

<AttributeFilterPolicy id="FilterPolicyObject-Proxy-FromAzure-byIssuer-Type">
    <PolicyRequirementRule xsi:type="Issuer" value="https://sts.windows.net/<sts_tenant_id>/" />
         
    <AttributeRule attributeID="azureDisplayname" permitAny="true" />
    <AttributeRule attributeID="azureGivenname" permitAny="true" />
    <AttributeRule attributeID="azureSurname" permitAny="true" />
    <AttributeRule attributeID="azureAuthnmethodsreferences" permitAny="true" />
    <AttributeRule attributeID="azureIdentityprovider" permitAny="true" />
    <AttributeRule attributeID="azureTenantid" permitAny="true" />
    <AttributeRule attributeID="azureEmailaddress" permitAny="true" />
    <AttributeRule attributeID="azureObjectidentifier" permitAny="true" />
    <AttributeRule attributeID="azureName">
        <PermitValueRule xsi:type="ScopeMatchesShibMDScope" />
    </AttributeRule>
</AttributeFilterPolicy>

Proxy Task 3. Enable IdP to Recognize Azure AD Claims 

Azure AD issues SAML assertions, however they are presented in an Azure/WS-Fed-centric naming convention (as "claims").

Our technique is to add in a new attribute mapping file that we can then use to parse the claims into internal attributes and in turn use them in the attribute resolver.

  1. Add a new file called in the IdP as attributes/azureClaims.xml if it doesn't already exist. This file contains each of your attributes from Azure AD that you want to use in the resolver.

The sample below is not exhaustive but has enough to get started on the claims file.


<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:util="http://www.springframework.org/schema/util"
       xmlns:p="http://www.springframework.org/schema/p"
       xmlns:c="http://www.springframework.org/schema/c"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
                           http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
                           http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util.xsd"
                           
       default-init-method="initialize"
       default-destroy-method="destroy">

    <bean parent="shibboleth.TranscodingRuleLoader">
    <constructor-arg>
    <list>
        <!-- claims relevant to person record -->
        <bean parent="shibboleth.TranscodingProperties">
            <property name="properties">
                <props merge="true">
                    <prop key="id">azureName</prop>
                    <prop key="transcoder">SAML2ScopedStringTranscoder</prop>
                    <prop key="saml2.name">http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name</prop>
                    <prop key="saml2.nameFormat">urn:oasis:names:tc:SAML:2.0:attrname-format:unspecified</prop>
                    <prop key="displayName.en">Name</prop>
                    <prop key="description.en">Azure UPN of an account expected to be scoped thus transcoded that way</prop>
                </props>
            </property>
        </bean>
        <bean parent="shibboleth.TranscodingProperties">
            <property name="properties">
                <props merge="true">
                    <prop key="id">azureEmailaddress</prop>
                    <prop key="transcoder">SAML2StringTranscoder</prop>
                    <prop key="saml2.name">http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress</prop>
                    <prop key="saml2.nameFormat">urn:oasis:names:tc:SAML:2.0:attrname-format:unspecified</prop>
                    <prop key="displayName.en">Mail</prop>
                    <prop key="description.en">Azure emailaddress field of an account</prop>
                </props>
            </property>
        </bean>
        <bean parent="shibboleth.TranscodingProperties">
            <property name="properties">
                <props merge="true">
                    <prop key="id">azureDisplayname</prop>
                    <prop key="transcoder">SAML2StringTranscoder</prop>
                    <prop key="saml2.name">http://schemas.microsoft.com/identity/claims/displayname</prop>
                    <prop key="saml2.nameFormat">urn:oasis:names:tc:SAML:2.0:attrname-format:unspecified</prop>
                    <prop key="displayName.en">Mail</prop>
                    <prop key="description.en">Azure displayname field of an account</prop>
                </props>
            </property>
        </bean>
        <bean parent="shibboleth.TranscodingProperties">
            <property name="properties">
                <props merge="true">
                    <prop key="id">azureGivenname</prop>
                    <prop key="transcoder">SAML2StringTranscoder</prop>
                    <prop key="saml2.name">http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname</prop>
                    <prop key="saml2.nameFormat">urn:oasis:names:tc:SAML:2.0:attrname-format:unspecified</prop>
                    <prop key="displayName.en">Given name</prop>
                    <prop key="description.en">Azure given name of an account</prop>
                </props>
            </property>
        </bean>
        <bean parent="shibboleth.TranscodingProperties">
            <property name="properties">
                <props merge="true">
                    <prop key="id">azureSurname</prop>
                    <prop key="transcoder">SAML2StringTranscoder</prop>
                    <prop key="saml2.name">http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname</prop>
                    <prop key="saml2.nameFormat">urn:oasis:names:tc:SAML:2.0:attrname-format:unspecified</prop>
                    <prop key="displayName.en">Surname</prop>
                    <prop key="description.en">Azure surname of an account</prop>
                </props>
            </property>
        </bean>

        <!-- default claims from Azure for any entity -->
        <bean parent="shibboleth.TranscodingProperties">
            <property name="properties">
                <props merge="true">
                    <prop key="id">azureTenantid</prop>
                    <prop key="transcoder">SAML2StringTranscoder</prop>
                    <prop key="saml2.name">http://schemas.microsoft.com/identity/claims/tenantid</prop>
                    <prop key="saml2.nameFormat">urn:oasis:names:tc:SAML:2.0:attrname-format:unspecified</prop>
                    <prop key="displayName.en">Tenant ID</prop>
                    <prop key="description.en">Azure tenantid</prop>
                </props>
            </property>
        </bean>
        <bean parent="shibboleth.TranscodingProperties">
            <property name="properties">
                <props merge="true">
                    <prop key="id">azureObjectidentifier</prop>
                    <prop key="transcoder">SAML2StringTranscoder</prop>
                    <prop key="saml2.name">http://schemas.microsoft.com/identity/claims/objectidentifier</prop>
                    <prop key="saml2.nameFormat">urn:oasis:names:tc:SAML:2.0:attrname-format:unspecified</prop>
                    <prop key="displayName.en">Object Identifier</prop>
                    <prop key="description.en">Azure object identifier of an account</prop>
                </props>
            </property>
        </bean>
         <bean parent="shibboleth.TranscodingProperties">
            <property name="properties">
                <props merge="true">
                    <prop key="id">azureIdentityprovider</prop>
                    <prop key="transcoder">SAML2StringTranscoder</prop>
                    <prop key="saml2.name">http://schemas.microsoft.com/identity/claims/identityprovider</prop>
                    <prop key="saml2.nameFormat">urn:oasis:names:tc:SAML:2.0:attrname-format:unspecified</prop>
                    <prop key="displayName.en">Identity Provider entityid</prop>
                    <prop key="description.en">Azure entityID of the tenant in Azure</prop>
                </props>
            </property>
        </bean>
        <bean parent="shibboleth.TranscodingProperties">
            <property name="properties">
                <props merge="true">
                    <prop key="id">azureAuthnmethodsreferences</prop>
                    <prop key="transcoder">SAML2StringTranscoder</prop>
                    <prop key="saml2.name">http://schemas.microsoft.com/claims/authnmethodsreferences</prop>
                    <prop key="saml2.nameFormat">urn:oasis:names:tc:SAML:2.0:attrname-format:unspecified</prop>
                    <prop key="displayName.en">Authnmethodsreferences</prop>
                    <prop key="description.en">Azure authentication method (password, modern auth? etc)</prop>
                </props>
            </property>
        </bean>


    </list>
    </constructor-arg>
    </bean>
    
</beans>


2. Update attributes/default-rules.xml to include this new file as a reference by adding this line to it after the last import:

<import resource="azureClaims.xml" />

3. Update attribute-resolver.xml to reveal our new attributes from Azure AD by adding a new DataConnector:

 <DataConnector id="passthroughAttributes" xsi:type="Subject" 
    exportAttributes="azureName azureEmailaddress azureDisplayname azureGivenname azureSurname azureTenantid azureObjectidentifier azureIdentityprovider azureAuthnmethodsreferences" />

Now that we have the the IdP able to interpret claims from Azure AD, we need to turn our attention to using them for user canonicalization and for populating our attributes.

Proxy Task 4. Subject Canonicalisation

This workflow takes the incoming assertion and extracts some data from it to work out the canonical (authoritative/normalized) username of the user logging in from Azure.

For AzureAD we are going to use the attribute claim: http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name which we just mapped internally in the IdP to azName

1. Add a new <AttributeDefinition> to attribute-resolver.xml as shown below.

This will transform the azName attribute from the Subject (the filtered data from the incoming assertion) into canonicalNameToUseForJoin attribute for use elsewhere:

<AttributeDefinition xsi:type="SubjectDerivedAttribute"
	forCanonicalization="true"
	id="canonicalNameToUseForJoin"
	principalAttributeName="azureName" />

2. Now configure which attributes to resolve during c14n:

Define the shibboleth.c14n.attribute.AttributesToResolve and shibboleth.c14n.attribute.AttributeSourceIds beans in c14n/attribute-sourced-subject-c14n-config.xml to the attribute ID of the above <AttributeDefinition>:

<util:list id="shibboleth.c14n.attribute.AttributesToResolve">
    <value>canonicalNameToUseForJoin</value>
</util:list>
<util:list id="shibboleth.c14n.attribute.AttributeSourceIds">
    <value>canonicalNameToUseForJoin</value>
</util:list>

In other proxy situations this may have more than one attribute but for Azure AD we are focused on a single one.

3. Review and ensure the bean in c14n/subject-c14n.xml has been uncommented:

<bean id="c14n/attribute" parent="shibboleth.PostLoginSubjectCanonicalizationFlow" /> 

At this point you should have a valid, locally understood username to pass into the "normal" resolver that already works, which is now available from places like:

$resolutionContext.principal

Proxy Task 5. Configuring Attribute pass-through and/or hybrid resolving

Attributes originating from Azure AD are referenced in attribute-resolver.xml like this:

Be sure to comment out the old definition of the names in the attribute-resolver.xml or you may get unexpected results.

<AttributeDefinition xsi:type="SubjectDerivedAttribute"
    forCanonicalization="false"
    id="mail"
    principalAttributeName="azureEmailaddress" />

<AttributeDefinition xsi:type="SubjectDerivedAttribute"
    forCanonicalization="false"
    id="displayName"
    principalAttributeName="azureDisplayname" />

<AttributeDefinition xsi:type="SubjectDerivedAttribute"
    id="eduPersonPrincipalName"
    principalAttributeName="azureName" />

Proxy Task 6. Handling REFEDS AuthnContext Requests (optional)

Azure does not currently have a documented way to influence the behavior of the AuthnContext in their SAML assertions.  However, Shibboleth provides the means to translate proxy requests and responses via authn/authn-comparison.xml. The example below handles REFEDS MFA requests:

<util:map id="shibboleth.PrincipalProxyRequestMappings">
	<entry>
		 <key>
			<bean parent="shibboleth.SAML2AuthnContextClassRef"
                  c:classRef="https://refeds.org/profile/mfa" />
		</key>
		<list>
			<bean parent="shibboleth.SAML2AuthnContextClassRef"
                  c:classRef="http://schemas.microsoft.com/claims/multipleauthn" />
		</list>
	</entry>
</util:map>
<util:map id="shibboleth.PrincipalProxyResponseMappings">
	<entry>
		 <key>
			<bean parent="shibboleth.SAML2AuthnContextClassRef"
                          c:classRef="http://schemas.microsoft.com/claims/multipleauthn" />
		</key>
		<list>
			<bean parent="shibboleth.SAML2AuthnContextClassRef"
                          c:classRef="https://refeds.org/profile/mfa" />
		</list>
	</entry>
</util:map>

Update the support matrix for the SAML authentication flow to understand the REFEDS MFA profile

(for v4.0.1) Update the authn/SAML bean in authn/general-authn.xml so it understands the REFEDS MFA profile by adding a supportedPrincipals property:

<bean id="authn/SAML" parent="shibboleth.AuthenticationFlow"
    p:nonBrowserSupported="false"
    p:passiveAuthenticationSupported="true"
    p:forcedAuthenticationSupported="true"
    p:proxyScopingEnforced="true"
    p:discoveryRequired="true">

    <property name="supportedPrincipals">
        <list>
            <bean parent="shibboleth.SAML2AuthnContextClassRef"
                c:classRef="urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport" />
            <bean parent="shibboleth.SAML2AuthnContextClassRef"
                c:classRef="urn:oasis:names:tc:SAML:2.0:ac:classes:Password" />
            <bean parent="shibboleth.SAML1AuthenticationMethod"
                c:method="urn:oasis:names:tc:SAML:1.0:am:password" />
            <bean parent="shibboleth.SAML2AuthnContextClassRef"
                c:classRef="https://refeds.org/profile/mfa" />
        </list>
    </property>
</bean>

Changes required in v4.1 may be different and you should look at the authn.properties file. More details here: SAMLAuthnConfiguration

Testing

Restart your IdP for your changes to take effect.  Because this is a SAML proxy configuration it doesn't make sense to use  aacli since it won't have the required information available to it.

Some sites have a developer account which has a tenant that can test entirely isolated from the production tenant. Alternatively you can create a different Azure AD non-gallery SAML app per IdP – one for production, one for pre-production etc. There's no hard and fast rule on the best way however the developer route is free and provides a great model for isolation between credentials as it's a different cookie domain entirely.

Related content / Recommended Reading