I'm building an on-boarding webapp to provision users for my LOB app. Most of my customers are "business customers", meaning they will ordinarily be directed to the v1 common endpoint by a custom policy, allowing them to auth against their own AAD tenant. The challenge is new users need follow-on provisioning in the LOB app as well (create db user, assign some permissions, etc).
What I'd like to do as part of on-boarding is call graphAPI to create what will become the federated user account in b2c, then with the new user objectId that comes back handle follow-on setup specific to my LOB app. Ideally when the user arrives for the first time, they would be redirected to auth against their own AAD, then map to the pre-created user in b2c, and finally land in the LOB app with an objectId that is already provisioned and ready.
Is this a supported scenario with some creative use of custom policies and graphAPI?
Thanks
Mark
You have the following options for this:
Create a local account user with the external email address and link the external user identity with this local account user.
Create an external account user with the external user identity.
1. Create a local account user with the external email address
Using Azure AD Graph API, you can create a local account user, with the signInNames property of the user object being set to the email address of the external user:
{
"accountEnabled": false,
"creationType": "LocalAccount",
"displayName": "John Smith",
"passwordProfile": {
"password": "a-strong-random-password",
"forceChangePasswordNextLogin": false
}
"signInNames": [
{
"type": "emailAddress",
"value": "john.smith#company.com"
}
]
}
Note: I recommend the accountEnabled property of the user object is set to true so that the end user can't log in with the local account password.
Using a custom policy, you can then add a new logic to find the local account user using the external email address and add the external user identity to this local account user, such as:
...
<!--
Find the external account user using the external user identity.
-->
<OrchestrationStep Order="16" Type="ClaimsExchange">
<Preconditions>
<Precondition Type="ClaimEquals" ExecuteActionsIf="true">
<Value>authenticationSource</Value>
<Value>localAccountAuthentication</Value>
<Action>SkipThisOrchestrationStep</Action>
</Precondition>
</Preconditions>
<ClaimsExchanges>
<ClaimsExchange Id="AADUserReadUsingAlternativeSecurityId" TechnicalProfileReferenceId="AAD-UserReadUsingAlternativeSecurityId-NoError" />
</ClaimsExchanges>
</OrchestrationStep>
<!--
If the external account user hasn't been found, then find the local account user using the external email address.
-->
<OrchestrationStep Order="17" Type="ClaimsExchange">
<Preconditions>
<Precondition Type="ClaimEquals" ExecuteActionsIf="true">
<Value>authenticationSource</Value>
<Value>localAccountAuthentication</Value>
<Action>SkipThisOrchestrationStep</Action>
</Precondition>
<Precondition Type="ClaimsExist" ExecuteActionsIf="true">
<Value>objectId</Value>
<Action>SkipThisOrchestrationStep</Action>
</Precondition>
</Preconditions>
<ClaimsExchanges>
<ClaimsExchange Id="AADUserReadUsingEmailAddress" TechnicalProfileReferenceId="AAD-UserReadUsingEmailAddress-NoError" />
</ClaimsExchanges>
</OrchestrationStep>
<!--
If an account user hasn't been found, then create an external account user with the external user identity.
-->
<OrchestrationStep Order="18" Type="ClaimsExchange">
<Preconditions>
<Precondition Type="ClaimEquals" ExecuteActionsIf="true">
<Value>authenticationSource</Value>
<Value>localAccountAuthentication</Value>
<Action>SkipThisOrchestrationStep</Action>
</Precondition>
<Precondition Type="ClaimsExist" ExecuteActionsIf="true">
<Value>objectId</Value>
<Action>SkipThisOrchestrationStep</Action>
</Precondition>
</Preconditions>
<ClaimsExchanges>
<ClaimsExchange Id="AADUserWriteUsingAlternativeSecurityId" TechnicalProfileReferenceId="AAD-UserWriteUsingAlternativeSecurityId" />
</ClaimsExchanges>
</OrchestrationStep>
<!--
If the local account user has been found using the external email address, then add the external user identity to this local account user.
-->
<OrchestrationStep Order="19" Type="ClaimsExchange">
<Preconditions>
<Precondition Type="ClaimEquals" ExecuteActionsIf="true">
<Value>authenticationSource</Value>
<Value>localAccountAuthentication</Value>
<Action>SkipThisOrchestrationStep</Action>
</Precondition>
<!-- The following claim is output from the AAD-UserWriteUsingAlternativeSecurityId technical profile. -->
<Precondition Type="ClaimsExist" ExecuteActionsIf="true">
<Value>newUserCreated</Value>
<Action>SkipThisOrchestrationStep</Action>
</Precondition>
<!-- The following claim is output from the AAD-UserReadUsingEmailAddress-NoError technical profile. -->
<Precondition Type="ClaimsExist" ExecuteActionsIf="false">
<Value>existingUserFoundByEmail</Value>
<Action>SkipThisOrchestrationStep</Action>
</Precondition>
</Preconditions>
<ClaimsExchanges>
<ClaimsExchange Id="AADUserWriteUserIdentity" TechnicalProfileReferenceId="AAD-UserWriteUserIdentity" />
</ClaimsExchanges>
</OrchestrationStep>
...
2. Create an external account user with the external user identity
Using Azure AD Graph API, you can create an external account user, with the userIdentities property of the user object being set to the object identifier of the external user:
{
"accountEnabled": false,
"displayName": "John Smith",
"mailNickname": "john.smith",
"otherMails": [
"john.smith#company.com"
],
"userIdentities": [
{
"issuer": "https://sts.windows.net/{their-tenant-object-id}/",
"issuerUserId": "{their-user-object-id}"
}
],
"userPrincipalName": "{guid}#{your-tenant-name}.onmicrosoft.com"
}
where issuerUserId must be set to the base64 encoding for the object identifier of the external user.
Note: In the Azure AD OpenID Connect technical profile, you might have to change the claim mapping for the socialIdpUserId claim from the sub claim to the oid claim, so that it matches the userIdentities.issuerUserId property of the user object:
<OutputClaim ClaimTypeReferenceId="socialIdpUserId" PartnerClaimType="oid" />
Related
I used a custom policy to create the login screen.
And we want to run MFA on a per user basis.
For example, I have two user account.(user1, user2)
User 1 wants to log in without using MFA.
User 2 applies MFA and wants to log in.
Both users then access the same URL for login.
Where can we set up an MFA for each user in this way?
I've been able to implement self-service per user TOTP MFA enrollment/unenrollment via custom policies (one policy for enrollment, second one for unenrollment).
The idea in my scenario was that it is up to the end user to decide if he would like to use TOTP MFA or not.
So far this has been only a PoC implementation but it was working fine during my tests.
It was based on a TOTP MFA sample available on GitHub - https://github.com/azure-ad-b2c/samples/tree/master/policies/totp.
Relevant parts of my UserJourneys:
Enable MFA UserJourney
<UserJourney Id="EnableMFA" DefaultCpimIssuerTechnicalProfileReferenceId="JwtIssuer">
<OrchestrationSteps>
//Default steps for signing in user with Local/Social account
[...]
<OrchestrationStep Order="5" Type="ClaimsExchange">
<ClaimsExchanges>
<ClaimsExchange Id="CheckAvailableDevices" TechnicalProfileReferenceId="AzureMfa-GetAvailableDevices" />
</ClaimsExchanges>
</OrchestrationStep>
<OrchestrationStep Order="6" Type="ClaimsExchange">
<Preconditions>
<Precondition Type="ClaimEquals" ExecuteActionsIf="true">
<Value>numberOfAvailableDevices</Value>
<Value>0</Value>
<Action>SkipThisOrchestrationStep</Action>
</Precondition>
</Preconditions>
<ClaimsExchanges>
<ClaimsExchange Id="CreateErrorResponse-MfaAlreadyEnabled" TechnicalProfileReferenceId="CreateErrorResponse-MfaAlreadyEnabled" />
</ClaimsExchanges>
</OrchestrationStep>
<OrchestrationStep Order="7" Type="SendClaims" CpimIssuerTechnicalProfileReferenceId="ReturnOAuth2Error">
<Preconditions>
<Precondition Type="ClaimEquals" ExecuteActionsIf="true">
<Value>numberOfAvailableDevices</Value>
<Value>0</Value>
<Action>SkipThisOrchestrationStep</Action>
</Precondition>
</Preconditions>
</OrchestrationStep>
<OrchestrationStep Order="8" Type="InvokeSubJourney">
<JourneyList>
<Candidate SubJourneyReferenceId="TotpFactor-Input" />
</JourneyList>
</OrchestrationStep>
<OrchestrationStep Order="9" Type="InvokeSubJourney">
<JourneyList>
<Candidate SubJourneyReferenceId="TotpFactor-Verify" />
</JourneyList>
</OrchestrationStep>
<OrchestrationStep Order="10" Type="SendClaims" CpimIssuerTechnicalProfileReferenceId="JwtIssuer" />
</OrchestrationSteps>
<ClientDefinition ReferenceId="DefaultWeb" />
</UserJourney>
Step 5 reads numberOfAvailableDevices which indicates wheter user is already enrolled (numberOfAvailableDevices=1) or not (numberOfAvailableDevices=0).
If user is already enrolled, steps 6 & 7 are creating and returning an error to notify user he cannot enable MFA as it is already enabled.
Steps 8 & 9 are enrolling user. These steps are taken from GitHub sample.
Disable MFA UserJourney
Here, the AzureMfaProtocolProvider technical provider doesn't support unenrollment of TOTP MFA. List of available operations can be found here - https://learn.microsoft.com/en-us/azure/active-directory-b2c/multi-factor-auth-technical-profile#totp-mode.
In order to unenroll user you can use Microsoft Graph (custom REST technical profile to your API is needed).
https://learn.microsoft.com/en-us/graph/api/resources/softwareoathauthenticationmethod?view=graph-rest-beta
Using these endpoints, you can list softwareOathMethods for specific user and delete them if needed. If user has TOTP MFA enabled, then MS Graph should return an array with 1 softwareOathMethod.
UserJourney:
<UserJourney Id="DisableMFA" DefaultCpimIssuerTechnicalProfileReferenceId="JwtIssuer">
<OrchestrationSteps>
//Default steps for signing in user with Local/Social account
[...]
<OrchestrationStep Order="5" Type="ClaimsExchange">
<ClaimsExchanges>
<ClaimsExchange Id="CheckAvailableDevices" TechnicalProfileReferenceId="AzureMfa-GetAvailableDevices" />
</ClaimsExchanges>
</OrchestrationStep>
<OrchestrationStep Order="6" Type="ClaimsExchange">
<Preconditions>
<Precondition Type="ClaimEquals" ExecuteActionsIf="false">
<Value>numberOfAvailableDevices</Value>
<Value>0</Value>
<Action>SkipThisOrchestrationStep</Action>
</Precondition>
</Preconditions>
<ClaimsExchanges>
<ClaimsExchange Id="CreateErrorResponse-MfaNotEnabled" TechnicalProfileReferenceId="CreateErrorResponse-MfaNotEnabled" />
</ClaimsExchanges>
</OrchestrationStep>
<OrchestrationStep Order="7" Type="SendClaims" CpimIssuerTechnicalProfileReferenceId="ReturnOAuth2Error">
<Preconditions>
<Precondition Type="ClaimEquals" ExecuteActionsIf="false">
<Value>numberOfAvailableDevices</Value>
<Value>0</Value>
<Action>SkipThisOrchestrationStep</Action>
</Precondition>
</Preconditions>
</OrchestrationStep>
<OrchestrationStep Order="8" Type="InvokeSubJourney">
<JourneyList>
<Candidate SubJourneyReferenceId="TotpFactor-Verify" />
</JourneyList>
</OrchestrationStep>
<OrchestrationStep Order="9" Type="ClaimsExchange">
<ClaimsExchanges>
<ClaimsExchange Id="REST-DisableMfa" TechnicalProfileReferenceId="REST-DisableMfa" />
</ClaimsExchanges>
</OrchestrationStep>
<OrchestrationStep Order="10" Type="SendClaims" CpimIssuerTechnicalProfileReferenceId="JwtIssuer" />
</OrchestrationSteps>
<ClientDefinition ReferenceId="DefaultWeb" />
</UserJourney>
Steps 5,6 & 7 are similar to corresponding steps in previously mentioned EnableMFA UserJourney.
Step 8 forces user to input TOTP code from his authenticator app before disabling MFA so we are sure he is the owner of the account.
In step 9, REST-DisableMfa technical profile is my custom REST api which then calls Microsoft Graph api.
Using the build-in user flows, it's not possible. You'll need to use a custom policy and allow the MFA based on some custom property (e.g. role admin / regular user).
Here's a link that can help you with some guidance related to implement custom MFA:
https://www.kallemarjokorpi.fi/blog/azure-b2c-custom-mfa-implementation.html
I would like to make a user journey for an "on the fly password migration" use case.
I have created a custom B2C attribute, this flag tells whether the password was already migrated from the legacy system or not. If this migration flag is set I would like to execute a normal "CombinedSignInAndSignUp" step for the local account. If the flag is not set I would like call my REST API to do the migration.
I can easily implement the two steps in separate user journeys, but I couldn't make it work when the two steps are in the same user journey. If my user journey contains more than one CombinedSignInAndSignUp step I get either a validation error or an error during execution, even though I set a precondition for the steps.
How can I add two steps with the type CombinedSignInAndSignUp to one user journey?
Or are there other ways to achieve what I would like to?
I didn't find any examples for this use case yet.
This can be implemented using preconditions within the SelfAsserted-LocalAccountSignin-Email technical profile.
Assuming the AAD-UserReadUsingEmailAddress technical profile outputs the custom attribute, then this custom attribute can determine whether the login-NonInteractive or REST-ValidateCredential validation technical profile is invoked.
<TechnicalProfile Id="SelfAsserted-LocalAccountSignin-Email">
<ValidationTechnicalProfiles>
<!-- Get the migration flag of the signing-in user. -->
<ValidationTechnicalProfile ReferenceId="AAD-UserReadUsingEmailAddress" />
<!-- If the migration flag is set, then authenticate against the local directory. -->
<ValidationTechnicalProfile ReferenceId="login-NonInteractive">
<Preconditions>
<Precondition Type="ClaimsExist" ExecuteActionsIf="false">
<Value>extension_AccountMigrated</Value>
<Action>SkipThisValidationTechnicalProfile</Action>
</Precondition>
<Precondition Type="ClaimEquals" ExecuteActionsIf="false">
<Value>extension_AccountMigrated</Value>
<Value>True</Value>
<Action>SkipThisValidationTechnicalProfile</Action>
</Precondition>
</Preconditions>
</ValidationTechnicalProfile>
<!-- If the migration flag isn't set, then authenticate against the remote directory. -->
<ValidationTechnicalProfile ReferenceId="REST-ValidateCredential">
<Preconditions>
<Precondition Type="ClaimsExist" ExecuteActionsIf="true">
<Value>extension_AccountMigrated</Value>
<Action>SkipThisValidationTechnicalProfile</Action>
</Precondition>
<Precondition Type="ClaimEquals" ExecuteActionsIf="true">
<Value>extension_AccountMigrated</Value>
<Value>True</Value>
<Action>SkipThisValidationTechnicalProfile</Action>
</Precondition>
</Preconditions>
</ValidationTechnicalProfile>
</ValidationTechnicalProfiles>
</TechnicalProfile>
I'm using custom policy in Identity Framework in Azure AD B2C. I have SignUpOrSignin.xml and TrustFrameworkBase.xml to custom the policies. I have activated MFA during sign-up but it also activated on sign-in. How can turn-off it during sign-in ?
You can execute the MFA step on sign-up but not execute it on sign-in by adding the newUser claim as a ClaimsExist pre-condition:
<OrchestrationStep Order="7" Type="ClaimsExchange">
<Preconditions>
<Precondition Type="ClaimsExist" ExecuteActionsIf="false">
<Value>newUser</Value>
<Action>SkipThisOrchestrationStep</Action>
</Precondition>
<Precondition Type="ClaimsExist" ExecuteActionsIf="true">
<Value>isActiveMFASession</Value>
<Action>SkipThisOrchestrationStep</Action>
</Precondition>
</Preconditions>
<ClaimsExchanges>
<ClaimsExchange Id="PhoneFactor-Verify" TechnicalProfileReferenceId="PhoneFactor-InputOrVerify" />
</ClaimsExchanges>
</OrchestrationStep>
The newUser claim is created during sign-up by one of the following technical profiles: LocalAccountSignUpWithLogonEmail, SelfAsserted-Social or AAD-UserWriteUsingAlternativeSecurityId. It won't exist during sign-in.
We have configured Facebook, LinkedIn, Twitter, Google and AzureAD as our providers using Custom SignUp/SignIn Policy.
We have a requirement to ask for Business Email when a user sign's up using social Idp(Facebook, LinkedIn, Twitter, Google) but not when Azure AD selected(as they are already using their business email).
I have added the new attribute to this section,
<TechnicalProfile Id="SelfAsserted-Social">
Please suggest.
Firstly, you must ascertain whether an enterprise account or a social account has been logged in.
For example:
<TechnicalProfile Id="ContosoProfile">
<OutputClaims>
<OutputClaim ClaimTypeReferenceId="authenticationSource" DefaultValue="contosoAuthentication" />
</OutputClaims>
</TechnicalProfile>
and:
<TechnicalProfile Id="Facebook-OAUTH">
<OutputClaims>
<OutputClaim ClaimTypeReferenceId="authenticationSource" DefaultValue="socialIdpAuthentication" />
</OutputClaims>
</TechnicalProfile>
Secondly, you must create two technical profiles for the account registration: one technical profile for the enterprise account registration that excludes the business email and the other technical profile for the social account registration that includes it.
For example:
<ClaimsProvider>
<DisplayName>Self Asserted</DisplayName>
<TechnicalProfiles>
<TechnicalProfile Id="SelfAsserted-Enterprise">
...
</TechnicalProfile>
<TechnicalProfile Id="SelfAsserted-Social">
...
</TechnicalProfile>
</TechnicalProfiles>
</ClaimsProvider>
Thirdly, based on the authenticationSource claim, you must invoke one or the other technical profile.
For example:
<UserJourney Id="SignUpOrSignIn">
<OrchestrationSteps>
<OrchestrationStep Order="4" Type="ClaimsExchange">
<Preconditions>
<Precondition Type="ClaimsExist" ExecuteActionsIf="true">
<Value>objectId</Value>
<Action>SkipThisOrchestrationStep</Action>
</Precondition>
<Precondition Type="ClaimEquals" ExecuteActionsIf="true">
<Value>authenticationSource</Value>
<Value>socialIdpAuthentication</Value>
<Action>SkipThisOrchestrationStep</Action>
</Precondition>
</Preconditions>
<ClaimsExchanges>
<ClaimsExchange Id="SelfAsserted-Enterprise" TechnicalProfileReferenceId="SelfAsserted-Enterprise" />
</ClaimsExchanges>
</OrchestrationStep>
<OrchestrationStep Order="5" Type="ClaimsExchange">
<Preconditions>
<Precondition Type="ClaimsExist" ExecuteActionsIf="true">
<Value>objectId</Value>
<Action>SkipThisOrchestrationStep</Action>
</Precondition>
<Precondition Type="ClaimEquals" ExecuteActionsIf="true">
<Value>authenticationSource</Value>
<Value>contosoAuthentication</Value>
<Action>SkipThisOrchestrationStep</Action>
</Precondition>
</Preconditions>
<ClaimsExchanges>
<ClaimsExchange Id="SelfAsserted-Social" TechnicalProfileReferenceId="SelfAsserted-Social" />
</ClaimsExchanges>
</OrchestrationStep>
</OrchestrationSteps>
</UserJourney>
Is it possible to skip a validation technical profile based on a condition?
In my case I have the username and password in a screen. I would like to validate the password only if the user meets a certain criteria. If not I would like to redirect the user to reset password page.
Yes, you can use preconditions in the User Journey to skip a step a particular Orchestration Step.
Here is an example of a user journey where if the claim of "requiresPasswordReset" is equal to true then the user is taken to the "LocalAccount-PasswordReset" technical profile. Otherwise if false, the user skips over this step to continue the user journey.
Example:
<OrchestrationStep Order="1" Type="ClaimsExchange">
<Preconditions>
<Precondition Type="ClaimEquals" ExecuteActionsIf="false">
<Value>requiresPasswordReset</Value>
<Value>True</Value>
<Action>SkipThisOrchestrationStep</Action>
</Precondition>
</Preconditions>
<ClaimsExchanges>
<ClaimsExchange Id="PasswordReset" TechnicalProfileReferenceId="LocalAccount-PasswordReset" />
</ClaimsExchanges>
</OrchestrationStep>
You can find further documentation on preconditions and user journeys here