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. |
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.
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:
When the user initiates sign-on the following usually happens:
The Shibboleth IdP observes it's configured for SAML sign on and redirects to an upstream IdP (Azure AD). Settings for this are controlled in
idp.properties
authn/saml-authn-config.xml
verifies that the assertion arrives from an entity it trusts as configured in metadata-providers.xml
attribute-filter.xml
AttributesToResolve
in the resolver and resolves each one.AttributeSourceIds
and picks the first valid one to be the principal's name to be used later$resolutionContext.principal
(eg. existing LDAP data connector, etc.) during "standard" attribute resolution processBe sure to have these assets in place and appropriate access to them before you start:
This document refers to some key settings and terms described here. For your installation have these on hand to speed up configuration:
https://idp.example.com/idp
https://sts.windows.net/<sts_tenant_id>/
SamAccountName
is often a good choice and appears as 'name' in claims.There are 3 main trust-related configurations to adjust:
3. the Azure AD trust to the Shibboleth IdP as a Relying Party
As your IdP will need act as an SP, you'll need extra blocks in your entity's metadata.
idp-metadata.xml
file to include a <SPSSODescriptor>
block. https://idp.example.com/idp
) with the base of your IdP.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> |
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:
https://idp.example.com/idp/shibboleth
(same as for your IdP )https://idp.example.com/idp/profile/Authn/SAML2/POST/SSO
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.
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:
2 SingleSignOnService elements
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
can be found in a package named
(xmllintlibxml2
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:
xmlns:shibmd="urn:mace:shibboleth:metadata:1.0
<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!
|
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. |
The following changes are needed to adjust the IdP to proxying behaviour:
There are three steps for this task:
target
:<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
The IdP will not ingest attributes from the Azure AD Upstream IdP unless they're allowed in by a filter.
<AttributeFilterPolicy>
to permit trusted attributes to be handled.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> |
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.
The sample below is not exhaustive but has enough to get started on the claims file.
<shibmd:Scope>
extension in the upstream IdP metadata.urn:oasis:names:tc:SAML:2.0:attrname-format:unspecified
as the claims don't carry the proper format on them.<?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.
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
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" /> |
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> |
(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
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.