Using a different Identity repository for authentication using Azure AD B2C
Note: This is a PoC that you should use as a guide. The code is not Production ready and you use it at your own risk.
A number of people have asked me if B2C can use another repository for authentication e.g. a SQL DB so I thought I would have a crack at it using a custom policy.
I’m using an Azure Function to do the authentication. In the real world, this is where you would do the actual authentication but for the purposes of the post, I’m just using canned credentials.
The username can be anything but the password needs to be “password” for the authentication to proceed.
I’m building up a library of Azure Functions to use with B2C. This function is called “RepositoryAuth”.
We check the password:
if (password.Equals("password"))
{
return (ActionResult)new OkObjectResult(new ResponseContentJWT());
}
ResponseContentJWT looks like:
public ResponseContentJWT()
{
this.displayName = "Joe Bloggs";
this.givenName = "Joe";
this.surName = "Bloggs";
this.role = "Admin";
this.objectId = "83a7d054-7129-4d11-9403-53bbccbb7f19";
}
If the password is incorrect, we return:
return (ActionResult)new ConflictObjectResult(new ResponseContent("Invalid credentials", 409, "API12345", "50f0bd91-2ff4-4b8f-828f-00f170519ddb",
"Credentials are incorrect",
"https://info/api/API12345"));
where “ResponseContent” looks like:
public ResponseContent(string userMessage, int status, string code, string requestId, string developerMessage, string moreInfo)
{
this.version = ResponseContent.ApiVersion;
this.userMessage = userMessage;
this.status = status;
this.code = code;
this.requestId = requestId;
this.developerMessage = developerMessage;
this.moreInfo = moreInfo;
}
As usual, the custom policy is in a gist.
The user journey only has two steps:
<UserJourneys>
<UserJourney Id="SignUpOrSignIn_REST_Auth">
<OrchestrationSteps><OrchestrationStep Order="1" Type="ClaimsExchange">
<ClaimsExchanges>
<ClaimsExchange Id="SignIn" TechnicalProfileReferenceId="SelfAsserted-Account-Signin-Auth"/>
</ClaimsExchanges>
</OrchestrationStep><OrchestrationStep Order="2" Type="SendClaims" CpimIssuerTechnicalProfileReferenceId="JwtIssuer"/>
</OrchestrationSteps>
<ClientDefinition ReferenceId="DefaultWeb"/>
</UserJourney>
</UserJourneys>
Basically, a sign-in page and then send the claims.
The first step is:
<TechnicalProfile Id="SelfAsserted-Account-Signin-Auth">
<DisplayName>Account Signin</DisplayName>
<Protocol Name="Proprietary"
Handler="Web.TPEngine.Providers.SelfAssertedAttributeProvider, Web.TPEngine, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null"/>
<Metadata>
<Item Key="IpAddressClaimReferenceId">IpAddress</Item>
<Item Key="ContentDefinitionReferenceId">api.selfasserted</Item>
</Metadata>
<IncludeInSso>false</IncludeInSso>
<DisplayClaims>
<DisplayClaim ClaimTypeReferenceId="signInName"
Required="true"/>
<DisplayClaim ClaimTypeReferenceId="password"
Required="true"/>
</DisplayClaims>
<OutputClaims>
<OutputClaim ClaimTypeReferenceId="displayName"/>
<OutputClaim ClaimTypeReferenceId="givenName"/>
<OutputClaim ClaimTypeReferenceId="surname"/>
<OutputClaim ClaimTypeReferenceId="role"/>
<OutputClaim ClaimTypeReferenceId="objectId"/>
</OutputClaims>
<ValidationTechnicalProfiles>
<ValidationTechnicalProfile ReferenceId="REST-API-Repository-Auth"/>
</ValidationTechnicalProfiles>
<UseTechnicalProfileForSessionManagement ReferenceId="SM-AAD"/>
</TechnicalProfile>
So we use DisplayClaims to get the username and password and the output claims are the result of calling the API that validates the credentials.
Note that B2C has a special way of handling the password.
Essentially, it cannot be passed between steps. You have to call the API in a validation technical profile in the SelfAsserted TP.
As per this documentation:
“For security reasons, a password claim value (UserInputType
set to Password
) is available only to the self-asserted technical profile's validation technical profiles. You cannot use password claim in the next orchestration steps.”
You could of course copy it to another claim if you needed it in a later step. (But be aware of security implications).
The API is called in the ValidationTechnicalProfile.
It looks like:
<ClaimsProvider>
<DisplayName>REST APIs Repository Auth</DisplayName>
<TechnicalProfiles>
<!-- Custom Restful service -->
<TechnicalProfile Id="REST-API-Repository-Auth">
<DisplayName>Date conversion</DisplayName>
<Protocol Name="Proprietary" Handler="Web.TPEngine.Providers.RestfulProvider, Web.TPEngine, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null"/>
<Metadata>
<Item Key="ServiceUrl">https://azureb2cfunctions123456.azurewebsites.net/api/RepositoryAuth</Item>
<Item Key="AuthenticationType">None</Item>
<Item Key="HttpBinding">POST</Item>
<Item Key="SendClaimsIn">Form</Item>
<Item Key="AllowInsecureAuthInProduction">true</Item>
<Item Key="DebugMode">true</Item>
</Metadata>
<InputClaims>
<InputClaim ClaimTypeReferenceId="signInName" PartnerClaimType="userName" Required="true"/>
<InputClaim ClaimTypeReferenceId="password" PartnerClaimType="password" Required="true"/>
</InputClaims>
<OutputClaims>
<OutputClaim ClaimTypeReferenceId="displayName"/>
<OutputClaim ClaimTypeReferenceId="givenName"/>
<OutputClaim ClaimTypeReferenceId="surname"/>
<OutputClaim ClaimTypeReferenceId="role"/>
<OutputClaim ClaimTypeReferenceId="objectId"/>
</OutputClaims>
<UseTechnicalProfileForSessionManagement ReferenceId="SM-Noop"/>
</TechnicalProfile>
</TechnicalProfiles>
</ClaimsProvider>
</ClaimsProviders>
So we call the “RepositoryAuth” function and return the contents of “ResponseContentJWT” as above.
The RelyingParty simply returns the contents of “ResponseContentJWT” as claims.
<RelyingParty>
<DefaultUserJourney ReferenceId="SignUpOrSignIn_REST_Auth"/>
<UserJourneyBehaviors>
<JourneyInsights TelemetryEngine="ApplicationInsights" InstrumentationKey="410...5d0" DeveloperMode="true" ClientEnabled="false" ServerEnabled="true" TelemetryVersion="1.0.0"/>
</UserJourneyBehaviors>
<TechnicalProfile Id="PolicyProfile">
<DisplayName>PolicyProfile</DisplayName>
<Protocol Name="OpenIdConnect"/>
<OutputClaims>
<OutputClaim ClaimTypeReferenceId="displayName"/>
<OutputClaim ClaimTypeReferenceId="givenName"/>
<OutputClaim ClaimTypeReferenceId="surname"/>
<OutputClaim ClaimTypeReferenceId="role"/>
<OutputClaim ClaimTypeReferenceId="objectId" PartnerClaimType="sub"/>
</OutputClaims>
<SubjectNamingInfo ClaimType="sub"/>
</TechnicalProfile>
</RelyingParty>
So lets try out the custom policy using the B2C “Run” function.
This returns:
Notice that the “sub” is the objectId returned from the function.
Using an invalid password returns:
that matches the error return of the function.
So lets complete the journey by calling the custom policy from an actual application.
I used this B2C sample.
Following the instructions in the Readme and then running it shows:
Clicking “Sign in” shows our login screen:
and then B2C returns:
where the display name and claims are displayed.
All good!