ocano.net.

AZURE AD B2C CUSTOM POLICIES (SERIES)

SAML2 SIGN-IN ONLY

Cover Image for SAML2 SIGN-IN ONLY

TLDR;

The post shows additions I made to the 'Extensions file' to support sign-in only via SAML service providers. It also shows the corresponding changes I had to make to a 'Relying party file' in order for it to work with a new user journey and the 'email' claim as a unique identifier.

If you just want to see the code, check out the TrustFrameworkExtensions.xml file and the SigninSAML.xml file in the source code repository.

Introduction

An important requirement for the Azure AD B2C (AADB2C) tenant I set up, was that it was to be configured to act as an identity provider (idP) to multiple applications. Some of the applications were configured to use OpenID Connect protocol, while others were configured up to use SAML2 protocol.

In this post I will focus on some of the configurations I have needed to make in order to compensate for the fact that I do not have access to configure the SAML-based service providers.

Prerequisites

If you haven't done so already, go read my other post Azure AD B2C Custom Policies (Series): Preface.

I have used the really-easy-to-follow guide Register a SAML application in Azure AD B2C. In the following, I will assume you've done so as well and your AADB2C is configured similarly.

Sign-in only

The reason for limiting the custom policy to sign-in only, is the way that AADB2C's unified interactions with the system work in some scenarios. In this case, the SignUpOrSignIn user journey that unifies options to sign-up, sign-in and reset password.

When a user clicks on the 'Forgot password?' link on the main page (sign-in ), AADB2C returns error code 'AADB2C90118' to the relying party application (ref. 1), which the application must then handle.

I could have tried to make the 'SignUpOrSignIn' user journey work for the service providers, but there is no requirement for the user to be able sign-up or reset their password in those applications.

Thus, I have decided to add a sign-in only user journey to be used in the SAML-based custom policy I already configured prior to being in this situation.

In the following I will specify the configurations that I have made in the 'Extensions file' and the 'Relying party file'.

Extensions file

First I have added a ContentDefinition:

<ContentDefinition Id="api.signin">
    <LoadUri>https://__BlobBaseUri__/templates/selfAsserted.html</LoadUri>
    <RecoveryUri>~/common/default_page_error.html</RecoveryUri>
    <DataUri>urn:com:microsoft:aad:b2c:elements:selfasserted:1.1.0</DataUri>
    <Metadata>
        <Item Key="DisplayName">Sign-in page</Item>
    </Metadata>
    <LocalizedResourcesReferences MergeBehavior="Prepend">
        <LocalizedResourcesReference Language="da" LocalizedResourcesReferenceId="api.signin.da" />
    </LocalizedResourcesReferences>
</ContentDefinition>

Following that, I have added a LocalizationResources element corresponding to the one referenced in the ContentDefinition:

<LocalizedResources Id="api.signin.da">
    <LocalizedStrings>
        <LocalizedString ElementType="ClaimType" ElementId="signInName" StringId="DisplayName">E-mail</LocalizedString>
        <LocalizedString ElementType="ClaimType" ElementId="password" StringId="DisplayName">Adgangskode</LocalizedString>
        <LocalizedString ElementType="ErrorMessage" StringId="UserMessageIfInvalidPassword">Den udfyldte adgangskode er ikke gyldig</LocalizedString>
        <LocalizedString ElementType="ErrorMessage" StringId="UserMessageIfClaimsPrincipalDoesNotExist">Vi kan ikke finde din konto</LocalizedString>
    </LocalizedStrings>
</LocalizedResources>

Lastly, I have added a new UserJourney:

<UserJourney Id="SignIn">
    <OrchestrationSteps>

        <OrchestrationStep Order="1" Type="CombinedSignInAndSignUp" ContentDefinitionReferenceId="api.signin">
            <ClaimsProviderSelections>
                <ClaimsProviderSelection ValidationClaimsExchangeId="LocalAccountSigninEmailExchange" />
            </ClaimsProviderSelections>
            <ClaimsExchanges>
                <ClaimsExchange Id="LocalAccountSigninEmailExchange" TechnicalProfileReferenceId="SelfAsserted-LocalAccountSignin-Email" />
            </ClaimsExchanges>
        </OrchestrationStep>

        <!-- This step reads any user attributes that we may not have received when in the token. -->
        <OrchestrationStep Order="2" Type="ClaimsExchange">
            <ClaimsExchanges>
                <ClaimsExchange Id="AADUserReadWithObjectId" TechnicalProfileReferenceId="AAD-UserReadUsingObjectId" />
            </ClaimsExchanges>
        </OrchestrationStep>

        <OrchestrationStep Order="3" Type="SendClaims" CpimIssuerTechnicalProfileReferenceId="JwtIssuer" />

    </OrchestrationSteps>
    <ClientDefinition ReferenceId="DefaultWeb" />
</UserJourney>

I will now go on to wrap the configuration up in a 'Relying party file'.

SIGNINSAML.XML

Initially when I created this file, I called it 'SignUpOrSigninSAML.xml'. I did so because the relying party file was referencing the 'SignUpOrSignIn' user journey in the guide (ref. 2) I had followed.

I have now rename the file to 'SigninSAML.xml' and changed the configuration of the 'UserJouney' element to the following:

<UserJourneys>
    <UserJourney Id="SignIn">
        <OrchestrationSteps>
            <OrchestrationStep Order="3" Type="SendClaims" CpimIssuerTechnicalProfileReferenceId="Saml2AssertionIssuer"/>
        </OrchestrationSteps>
    </UserJourney>
</UserJourneys>

The final 'Relying party file' ends up looking like this:

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<TrustFrameworkPolicy xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:xsd="http://www.w3.org/2001/XMLSchema"
    xmlns="http://schemas.microsoft.com/online/cpim/schemas/2013/06" PolicySchemaVersion="0.3.0.0" 
    TenantId="__TenantId__" 
    PolicyId="B2C_1A_signup_signin_saml"
    PublicPolicyUri="http://__TenantId__/B2C_1A_signup_signin_saml">

    <BasePolicy>
        <TenantId>__TenantId__</TenantId>
        <PolicyId>B2C_1A_TrustFrameworkExtensions</PolicyId>
    </BasePolicy>

    <UserJourneys>
        <UserJourney Id="SignIn">
            <OrchestrationSteps>
                <OrchestrationStep Order="3" Type="SendClaims" CpimIssuerTechnicalProfileReferenceId="Saml2AssertionIssuer"/>
            </OrchestrationSteps>
        </UserJourney>
    </UserJourneys>

    <RelyingParty>
        <DefaultUserJourney ReferenceId="SignIn" />
        <TechnicalProfile Id="PolicyProfile">
            <DisplayName>PolicyProfile</DisplayName>
            <Protocol Name="SAML2"/>
            <OutputClaims>
                <OutputClaim ClaimTypeReferenceId="signInNames.emailAddress" PartnerClaimType="email" DefaultValue="" />
                <OutputClaim ClaimTypeReferenceId="objectId" PartnerClaimType="objectId"/>
            </OutputClaims>
            <SubjectNamingInfo ClaimType="email" Format="urn:oasis:names:tc:SAML:2.0:nameid-format:persistent" ExcludeAsClaim="true"/>
        </TechnicalProfile>
    </RelyingParty>
</TrustFrameworkPolicy>

It differs from the one in the guide (ref. 2) in one other aspect.

I have had to change the 'SubjectNamingInfo' element in the 'RelyingParty' element, due to an error thrown by a service provider.

<SubjectNamingInfo ClaimType="email" Format="urn:oasis:names:tc:SAML:2.0:nameid-format:persistent" ExcludeAsClaim="true"/>

My assumption is that the service provider does not have a unique claim identifier specified in the configuration for the application. As a result, the AADB2C user's objectId claim is assumed to be the unique claim identifier as this is how it is configured out-of-the-box for all custom policy 'Relying party files' in the starter pack.

Specifying 'email' as the 'SubjectNamingInfo' element's 'ClaimType' attribute value is a way to resolve the issue without the service provider application's configuration being changed.

That concludes the notes I have from configuring AADB2C as an idP for SAML service providers.

Next steps

If you haven't already read it, you might like a more general post from this series Azure AD B2C Custom Policies (Series): Extension File.

Otherwise, there's one about other 'Relying party files' called Azure AD B2C Custom Policies (Series): Starter Pack Relying Party Files.

For C# developers there's a post in the series called Azure AD B2C Custom Policies (Series): Basic User Operations With Microsoft Graph API

References

  1. User flows in Azure Active Directory B2C
  2. Register a SAML application in Azure AD B2C