Working with custom attributes in Azure AD B2C custom policies
Azure AD B2C
The Azure AD B2C directory comes with a built-in set of attributes. Some examples are given name, surname and userPrincipalName.
However, you often need to create your own e.g. for a use case where a REST API call returns a status and you need to persist this in B2C so that other parts of the system can use it to decide what actions they need to take.
For built-in policies, you can define custom attributes by adding new ones under “User attributes”.
There’s a good article here that describes the custom policy process for an edit profile.
Our use case is:
- During registration, we call a back-end REST API to check some details about the person registering
- The back-end API returns a status or an error message
- If there is an error message, the message is displayed on the B2C registration screen and the user is not registered
- If no error message, a status is returned and the user is registered
- This status is persisted to B2C and is returned as a claim in the token
- Other parts of the system may alter the status via the Graph API
- When a user logs in, the current status is returned as a claim in the token
Following the article, I didn’t create a new extensions application. I simply used the id’s from the existing b2c-extensions-app as described under “Next steps”.
Custom policies
The following builds on the custom policy process article. In our case we want to use the SignUpOrSignIn flow rather then the ProfileEdit.
For the custom policies (assuming no error), in both TrustFrameworkBase and TrustFrameworkExtensions, we add a new ClaimType:
<ClaimType Id=”extension_isValid”>
<DisplayName>extension_isValid</DisplayName>
<DataType>boolean</DataType>
<UserHelpText>Status of back-end update</UserHelpText>
<UserInputType>RadioSingleSelect</UserInputType>
</ClaimType>
In the SignUpOrSignIn file, we add an extra OutputClaim.
<OutputClaim ClaimTypeReferenceId=”objectId” PartnerClaimType=”sub”/>
<OutputClaim ClaimTypeReferenceId=”extension_isValid” />
In TrustFrameworkBase, for the technical profile SelfAsserted-LocalAccountSignin-Email, add input and output claims:
<TechnicalProfile Id=”SelfAsserted-LocalAccountSignin-Email”>
<DisplayName>Local 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=”SignUpTarget”>SignUpWithLogonEmailExchange</Item>
<Item Key=”setting.operatingMode”>Email</Item>
<Item Key=”ContentDefinitionReferenceId”>api.selfasserted</Item>
</Metadata>
<IncludeInSso>false</IncludeInSso>
<InputClaims>
<InputClaim ClaimTypeReferenceId=”signInName” />
<InputClaim ClaimTypeReferenceId=”extension_isValid”/>
</InputClaims>
<OutputClaims>
<OutputClaim ClaimTypeReferenceId=”signInName” Required=”true” />
<OutputClaim ClaimTypeReferenceId=”password” Required=”true” />
<OutputClaim ClaimTypeReferenceId=”objectId” />
<OutputClaim ClaimTypeReferenceId=”authenticationSource” />
<OutputClaim ClaimTypeReferenceId=”extension_isValid”/>
In TrustFrameworkBase, for the technical profile AAD-UserWriteUsingLogonEmail, add a persisted claim:
<! — Optional claims. →
<PersistedClaim ClaimTypeReferenceId=”givenName” />
<PersistedClaim ClaimTypeReferenceId=”surname” />
<PersistedClaim ClaimTypeReferenceId=”extension_isValid” />
In TrustFrameworkBase, for the technical profile AAD-UserWriteProfileUsingObjectId, add a persisted claim:
<! — Optional claims. →
<PersistedClaim ClaimTypeReferenceId=”givenName” />
<PersistedClaim ClaimTypeReferenceId=”surname” />
<PersistedClaim ClaimTypeReferenceId=”extension_isValid” />
In TrustFrameworkBase, for the technical profile LocalAccountSignUpWithLogonEmail, add input and output claims:
<InputClaims>
<InputClaim ClaimTypeReferenceId=”email” />
<InputClaim ClaimTypeReferenceId=”extension_isValid”/>
</InputClaims>
<OutputClaims>
<OutputClaim ClaimTypeReferenceId=”objectId” />
<OutputClaim ClaimTypeReferenceId=”email” PartnerClaimType=”Verified.Email” Required=”true” />
<OutputClaim ClaimTypeReferenceId=”newPassword” Required=”true” />
<OutputClaim ClaimTypeReferenceId=”reenterPassword” Required=”true” />
<OutputClaim ClaimTypeReferenceId=”executed-SelfAsserted-Input” DefaultValue=”true” />
<OutputClaim ClaimTypeReferenceId=”authenticationSource” />
<OutputClaim ClaimTypeReferenceId=”newUser” />
<! — Optional claims, to be collected from the user →
<OutputClaim ClaimTypeReferenceId=”displayName” />
<OutputClaim ClaimTypeReferenceId=”givenName” />
<OutputClaim ClaimTypeReferenceId=”surName” />
<OutputClaim ClaimTypeReferenceId=”extension_isValid”/>
In TrustFrameworkBase, for the technical profile AAD-UserReadUsingEmailAddress, add an output claim:
<OutputClaim ClaimTypeReferenceId=”surname” />
<OutputClaim ClaimTypeReferenceId=”extension_isValid” />
That’s the end (thankfully!).
REST API
The REST API is a web API in Azure App Services. There is a good article describing how to develop, deploy and integrate the API into custom policies.
In our case, the OutputClaimsModel (as per the article) simply returns a status.
public class OutputClaimsModel
{
public bool extension_isValid { get; set; }
}
The flag is called “isValid” but we prefix this with “extension” to conform with the standard.
For consistency, we also add this via the built-in policies:
We return an error message if something is wrong e.g.
if (error)
{
return Content(HttpStatusCode.Conflict, new B2CResponseContent("Test name is not valid, please provide a valid name", HttpStatusCode.Conflict));
}
and the user sees:
For the REST API, using the TrustFrameworkExtensions file:
<ClaimsProvider>
<DisplayName>REST APIs</DisplayName>
<TechnicalProfiles>
<! — Custom Restful service →
<TechnicalProfile Id=”REST-API-SignUp”>
<DisplayName>Validate user’s input data and return extension_isValid claim</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://your-web-site.azurewebsites.net/api/identity/signup</Item>
<Item Key=”AuthenticationType”>None</Item>
<Item Key=”SendClaimsIn”>Body</Item>
</Metadata>
<InputClaims>
<InputClaim ClaimTypeReferenceId=”email” />
<InputClaim ClaimTypeReferenceId=”givenName” PartnerClaimType=”firstName” />
<InputClaim ClaimTypeReferenceId=”surname” PartnerClaimType=”lastName” />
</InputClaims>
<OutputClaims>
<OutputClaim ClaimTypeReferenceId=”extension_isValid” PartnerClaimType=”extension_isValid” />
</OutputClaims>
<UseTechnicalProfileForSessionManagement ReferenceId=”SM-Noop” />
</TechnicalProfile>
<! — Change LocalAccountSignUpWithLogonEmail technical profile to support your validation technical profile →
<TechnicalProfile Id=”LocalAccountSignUpWithLogonEmail”>
<OutputClaims>
<OutputClaim ClaimTypeReferenceId=”extension_isValid” PartnerClaimType=”extension_isValid” />
</OutputClaims>
<ValidationTechnicalProfiles>
<ValidationTechnicalProfile ReferenceId=”REST-API-SignUp” />
</ValidationTechnicalProfiles>
</TechnicalProfile>
</TechnicalProfiles>
</ClaimsProvider>
OK, that’s the configuration finished.
Graph API
In terms of having a look at the extension attributes in B2C, we use the B2CGraphClient utility for accessing B2C AAD via the graph API.
The possible commands are:
b2c help
Get-User : Read users from your B2C directory. Optionally accepts an ObjectId as a 2nd argument, and query expression as a 3rd argument.
Create-User : Create a new user in your B2C directory. Requires a path to a .json file which contains required and optional information as a 2nd argument.
Update-User : Update an existing user in your B2C directory. Requires an objectId as a 2nd argument & a path to a .json file as a 3rd argument.
Delete-User : Delete an existing user in your B2C directory. Requires an objectId as a 2nd argument.
Get-Extension-Attribute : Lists all extension attributes in your B2C directory. Requires the b2c-extensions-app objectId as the 2nd argument.
Get-B2C-Application : Get the B2C Extensions Application in your B2C directory, so you can retrieve the objectId and pass it to other commands.
Help : Prints this help menu.
Syntax : Gives syntax information for each command, along
with examples.
b2c syntax
- Square brackets indicate cmd optional arguments
- Curly brackets indicate valid choices for a parameter
- For information on supported query expressions, including $filter, $top, $orderby, and $expand, see https://msdn.microsoft.com/en-us/library/azure/dn727074.aspx
- To find the objectId of the b2c-extensions-app, run Get-B2C-Application
Get-User : B2C Get-User [UserObjectId || Query]
: B2C Get-User 6d51065f-2e1d-4707–8ec9-ad491bae55ddCreate-User : B2C Create-User RelativePathToJson
: B2C Create-User ..\..\..\usertemplate-email.json
Update-User : B2C Update-User UserObjectId RelativePathToJson
: B2C Update-User 6d51065f-2e1d-4707–8ec9-ad491bae55dd ..\..\..usertemplate-email.json
Delete-User : B2C Delete-User UserObjectId
: B2C Delete-User 6d51065f-2e1d-4707–8ec9-ad491bae55dd
Get-Extension-Attribute : B2C Get-Extension-Attribute B2CExtensionsApplicationObjectId
: B2C Get-Extension-Attribute 909544d8-f8c0–49c7-b1
37-a89faff6f882
Get-B2C-Application : B2C Get-B2C-Application
Help : B2C Help
Syntax : B2C Syntax
So let’s run:
b2c Get-B2C-Application
GET https://graph.windows.net/tenant.onmicrosoft.com/applications?api-version=1.6&$filter=startswith(displayName, ‘b2c-extensions-app’)
Authorization: Bearer eyJUhH…
200: OK
{
“odata.metadata”: “https://graph.windows.net/tenant.onmicrosoft.com/$metadata#directoryObjects”,
“value”: [ {
“odata.type”: “Microsoft.DirectoryServices.Application”,
“objectType”: “Application”,
“objectId”: “3cc…17b”,
“deletionTimestamp”: null,
“acceptMappedClaims”: null,
“addIns”: [],
“appId”: “199…770”,
“appRoles”: [],
“availableToOtherTenants”: false,
“displayName”: “b2c-extensions-app. Do not modify. Used by AADB2C for storing user data.”,
…
Notice this displays information about the standard b2c-extensions-app. We need the objectId.
Now we run:
b2c Get-Extension-Attribute 3cc…17b
GET https://graph.windows.net/tenant.onmicrosoft.com/applications/3cc…17b/extensionProperties?api-version=1.6
Authorization: Bearer eyJUhH…
200: OK
{
“odata.metadata”: “https://graph.windows.net/tenant.onmicrosoft.com/$metadata#directoryObjects”,
“value”: [ {
“odata.type”: “Microsoft.DirectoryServices.ExtensionProperty”,
“objectType”: “ExtensionProperty”,
“objectId”: “68e…b5a”,
“deletionTimestamp”: null,
“appDisplayName”: “”,
“name”: “extension_199…770_isValid”,
“dataType”: “Boolean”,
“isSyncedFromOnPremises”: false,
“targetObjects”: [
“User” ] },
and we see the extension attribute that we created via the portal. Notice that the format of the attribute in the graph API is:
extension_199…770_isValid i.e. extension_GUID_isValid
whereas the name we used in the custom policies is:
extension_isValid
Now if we wanted to update a user with this attribute, we could use:
b2c update-user 39d…865 update.json
using the user’s objectId.
(To get the objectId, we could use:
b2c get-user
and this will display all the users with all user’s attributes).
update.json looks like:
{
“extension_199…770_isValid”: “False”
}
The result of the update is:
PATCH https://graph.windows.net/tenant.onmicrosoft.com/users/39d,,,865?api-version=1.6
Authorization: Bearer eyJ…UhH…
Content-Type: application/json
{
“extension_199…770_isValid”: “False”
}
204: No Content
null
where “204” means OK.
Then running:
b2c get-user 39d…865
shows all the attributes and at the end:
“userState”: null,
“userStateChangedOn”: null,
“userType”: “Member”,
“extension_199e…770_isValid”: false
Running the flow end to end
So let’s run the flow using “Run now” after uploading all the new policies.
We are using the unified signup / sign-in policy.
We signup the user, validate the email and then enter the password etc.
After the new user has been added to B2C, the returned JWT shows:
and we can confirm this via Get-User:
“userState”: null,
“userStateChangedOn”: null,
“userType”: “Member”,
“extension_199…770_isValid”: true
This proves that the attribute has been persisted in B2C.
Signing in also returns the extension attribute.
Use case
Let’s revisit the use case.
- During registration, we call a back-end REST API to check some details about the person registering
Via the REST API as above
- The back-end API returns a status or an error message
Yes
- If there is an error message, the message is displayed on the B2C registration screen and the user is not registered
Yes — error message as above
- If no error message, a status is returned and the user is registered
Yes
- This status is persisted to B2C and is returned as a claim in the token
Yes
- Other parts of the system may alter the status via the Graph API
Yes via “B2C Update-User”
- When a user logs in, the current status is returned as a claim in the token
Yes
All good!