Unified Security Summary: Enhancing Visibility and Value
May 7, 2025GenAI for Scam & Fraud Detection
May 7, 2025Integrating the Entra External ID hybrid approach via custom policies with the Azure AD B2C MFA startup pack

Note that this post is about a PoC; it is not production-ready, so use it at your own risk!
Please read this post that explains the hybrid approach.
In a diagram:

The custom policy uses Entra External ID (EEID) as the DB rather than B2C, which makes migration much easier at some point.
Looking at the files in the MFA starter pack, we have:
- TrustFrameworkBase
- TrustFrameworkLocalization
- TrustFrameworkExtensions
- SignUpOrSignin
as the main flow.
The only one of these that has any “API’s” is the base.
Looking at that file, we see:
Read
- AAD-UserReadUsingAlternativeSecurityId
- AAD-UserReadUsingAlternativeSecurityId-NoError
- AAD-UserReadUsingEmailAddress
- AAD-UserReadUsingObjectId
Write
- AAD-UserWriteUsingAlternativeSecurityId
- AAD-UserWriteUsingLogonEmail
- AAD-UserWritePasswordUsingObjectId
- AAD-UserWriteProfileUsingObjectId
- AAD-UserWritePhoneNumberUsingObjectId
with a mixture of objectId and alternativeSecurityId as the primary keys.
objectId is an attribute.
alternativeSecurityId is a “generated” attribute, i.e. it is not persisted to the database.
It’s a combination of “issuerUserId” and “identityProvider”, both found in the “identities” string collection.
<ClaimsTransformation Id="CreateAlternativeSecurityId"
TransformationMethod="CreateAlternativeSecurityId">
<InputClaim ClaimTypeReferenceId="issuerUserId"
TransformationClaimType="key"/>
<InputClaim ClaimTypeReferenceId="identityProvider"
TransformationClaimType="identityProvider"/>
<OutputClaim ClaimTypeReferenceId="alternativeSecurityId"
TransformationClaimType="alternativeSecurityId"/>
To integrate the MFA starter pack with the hybrid approach, we need REST API’s for all the above.
Note: The MFA starter pack uses “PhoneFactor-InputOrVerify” as the MFA method. I could not get this to work as it clearly does something on the back end, but I don’t know what.
I used the “AzureMfa-SendSms” methods that were in the original sample.
Follow the steps in the sample to configure the application registrations, etc.
I have adapted the user journeys for:
- SignUpOrSignIn
- ProfileEdit
- PasswordReset
to use the hybrid approach.
Note: The base policy was not changed, but there are two parts that you need to override:
Value="cpim_{0}@{RelyingPartyTenantId}
Value="{0}@{RelyingPartyTenantId}
This gives you the B2C tenant ID value, whereas we need the EEID value.
The custom policies and code for the Azure function are in a GitHub repo.
The code handles:
- Local account signup via link on the login page
- Local account sign-in via login page using native authentication, not ROPC
- Social account logins via federation buttons on the login page
- MFA via SMS
- Password reset via the embedded link on the login page
- Password reset via RP
- Profile edit via RP
Note that this allows SMS MFA, which is currently not supported in EEID.
Debugging an Azure function is hard, so I have added lots of debugging output to the console.
Errors are returned via:
return new ConflictObjectResult(new B2CResponseModel($"Error: {ex.Message}",
HttpStatusCode.Conflict));
All TP are prefixed with “CIAM-” and REST API with “REST-CIAM”.
The REST API use the same pattern:
Write user into CIAM tenant
<Protocol Name="Proprietary"
Handler="Web.TPEngine.Providers.RestfulProvider, Web.TPEngine, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" />
https://496c-222-152-99-121.ngrok-free.app/api/ciamhelper
Body
<!-- Set AuthenticationType to Basic or ClientCertificate in production
environments -->
None
true
<InputClaim ClaimTypeReferenceId="method" AlwaysUseDefaultValue="true"
DefaultValue="read" />
with the “method” defining the method to call in the function.
Note that I use “Ngrok” to run on localhost rather than continually deploying a new function.
This means the policies are slower to execute than in native B2C.
This would also be true without “Ngrok” because of latency, since the function is separate from B2C.
There may also be Graph throttling.
The “GraphCallsFromB2CTenant” application registration needs the following permissions:

Running the policy:

Sign up
We select “Sign up now”.

Select “Create”.

Proof up via SMS.

Verify the code.

Code verified, and we get the JWT back.
Note that we use the B2C built-in MFA to verify a user whose data is stored in EEID.
Now, looking at the EEID tenant, we see the user we have just created:

Looking at the new identity:

The console logging shows:
C# HTTP trigger function processed a request.
Object Id Email HybridCreate14@company.co.nz Password somepassword Method
createUser Phone number Display name Hybrid Create14 Given name Hybrid
Surname Create14 UPN issuerAssignedId issuer mailNickname
method createUser
----------------------------------------------
Creating user
Display name Hybrid Create14 email HybridCreate14@company.co.nz
Result Microsoft.Graph.Beta.Models.User stringObjectId a33...1d6
[2025-04-30T02:19:53.320Z] Executed 'ciamHelper' (Succeeded,
Id=623...af95, Duration=2435ms)
[2025-04-30T02:19:55.073Z] Executing 'ciamHelper'
(Reason='This function was programmatically called via the host APIs.',
Id=d10...33d)
C# HTTP trigger function processed a request.
Object Id a33...1d6 Email HybridCreate14@company.co.nz Password
Method read Phone number Display name Given name Surname UPN
issuerAssignedId issuer mailNickname
method read
----------------------------------------------
ObjectId - Reading user a33...1d6
Given name Hybrid Surname Create14 Display name Hybrid Create14
Email HybridCreate14@company.co.nz Phone number
UPN cpim_074...de2@tenant.onmicrosoft.com
[2025-04-30T02:19:57.306Z] Executed 'ciamHelper' (Succeeded,
Id=d10...33d, Duration=2453ms)
[2025-04-30T02:19:57.877Z] Executing 'ciamHelper' (Reason=
'This function was programmatically called via the host APIs.',
Id=3a0...533)
C# HTTP trigger function processed a request.
Object Id a33...1d6 Email Password Method getPhone Phone number
Display name Given name Surname UPN issuerAssignedId issuer
mailNickname
method getPhone
----------------------------------------------
Getting phone number objectId a33...1d6
[2025-04-30T02:20:01.128Z] Executed 'ciamHelper' (Succeeded,
Id=3a0...533, Duration=3517ms)
[2025-04-30T02:22:19.999Z] Executing 'ciamHelper'
(Reason='This function was programmatically called via the host APIs.',
Id=55f...9c8)
C# HTTP trigger function processed a request.
Object Id a33...1d6 Email Password Method setPhone Phone number +64xxxxxxx
Display name Given name Surname UPN issuerAssignedId issuer
mailNickname
method setPhone
----------------------------------------------
Setting phone number +64xxxxxxx objectId a33...1d6
[2025-04-30T02:22:27.261Z] Executed 'ciamHelper' (Succeeded,
Id=55f...9c8, Duration=7474ms)
Sign in with hybrid B2C
Now, let’s sign in with this user.

Select sign in.
Note that this uses EEID native authentication, rather than ROPC. You can see this in the logging where “continuation_token” is used.

We are asked for MFA via SMS.

Verify the code.

The code is verified, and we are sent the JWT.
The console logging shows:
C# HTTP trigger function processed a request.
Object Id Email hybridcreate14@company.co.nz Password somepassword Method auth
Phone number Display name Given name Surname UPN issuerAssignedId
issuer mailNickname
----------------------------------------------
Authenticating user
Initiate URL: https://tenant.ciamlogin.com/tenant.onmicrosoft.com/oauth2/v2.0/
initiate
Initiate Request Body: {"client_id":"16a...208b","challenge_type":
"password redirect","username":"hybridcreate14@company.co.nz"}
Initiate Response Status Code: OK
Initiate Response Content: {"continuation_token":
"AQA...AA"}
jsonObject {
"token_type": "Bearer",
"scope": "profile openid email
00000003-0000-0000-c000-000000000000/User.Read",
"expires_in": 5197,
"ext_expires_in": 5197,
"access_token": "eyJ...vkg",
"refresh_token": "1.A...RvQ",
"id_token": "eyJ...YDg"
}
[2025-04-30T02:37:47.924Z] Executed 'ciamHelper' (Succeeded,
Id=f7d...2ef, Duration=2897ms)
[2025-04-30T02:37:49.792Z] Executing 'ciamHelper'
(Reason='This function was programmatically called via the host APIs.',
Id=e29...cad)
C# HTTP trigger function processed a request.
Object Id a33...1d6 Email Password Method read Phone number Display name
Given name Surname UPN issuerAssignedId issuer mailNickname
method read
----------------------------------------------
ObjectId - Reading user a33...1d6
Given name Hybrid Surname Create14 Display name Hybrid Create14
Email HybridCreate14@company.co.nz Phone number
UPN cpim_074...de2@tenant.onmicrosoft.com
[2025-04-30T02:37:52.104Z] Executed 'ciamHelper'
(Succeeded, Id=e29...cad, Duration=2490ms)
[2025-04-30T02:37:52.539Z] Executing 'ciamHelper'
(Reason='This function was programmatically called via the host APIs.',
Id=244...91a)
C# HTTP trigger function processed a request.
Object Id a33...1d6 Email Password Method getPhone Phone number
Display name Given name Surname UPN issuerAssignedId issuer
mailNickname
method getPhone
----------------------------------------------
Getting phone number objectId a33...1d6
Phone number Microsoft.Graph.Beta.Models.PhoneAuthenticationMethod
[2025-04-30T02:37:58.564Z] Executed 'ciamHelper'
Succeeded, Id=24466a54-36e3-45e5-995a-b4dc5eaa...91a, Duration=6220ms)
Sign in with EEID
Given that the user is now in EEID, we should be able to sign in using the login and password we created via the hybrid flow.

We sign in with a username and password, select whether to remain signed in, and then get the JWT, just as we expected!
Social sign in
You’ll see a Google button on the sign-in screen above, so let’s try a social login.
We get the Google login screen, authenticate, and then the CIAM-SelfAsserted-Social TP is called.

Notice I then change my first name to “Rory1”.
I then hit “Create” and go through MFA proof up.
Now, if we look at the users in EEID, we see:

and looking at the user details:

we see the first name has changed.
Looking at the user:

and then clicking on the “google.com” link:

we see that a new federated user has been created.
The console logging shows:
C# HTTP trigger function processed a request.
Object Id Email Password Method readAlternativeSecurityId Phone number
Display name Given name Surname UPN issuerAssignedId 11...51
issuer google.com mailNickname
method readAlternativeSecurityId
----------------------------------------------
Read AlternativeSecurityID
IssuerAssignedId: 11...51, Issuer: google.com
Users found: 0
[2025-04-30T23:49:34.041Z] Executed 'ciamHelper' (Succeeded,
Id=9d...e2, Duration=6259ms)
[2025-04-30T23:50:36.948Z] Executing 'ciamHelper'
(Reason='This function was programmatically called via the host APIs.',
Id=ec...7)
C# HTTP trigger function processed a request.
Object Id Email rbrayb@gmail.com Password Method writeAlternativeSecurityId
Phone number Display name Rory Braybrook Given name Rory1 Surname Braybrook
UPN cpim_bc...b9@tenant.onmicrosoft.com
issuerAssignedId 11...51 issuer google.com mailNickname unknown
method writeAlternativeSecurityId
----------------------------------------------
Write AlternativeSecurityID
ObjectId: , Issuer: google.com, IssuerAssignedId: 11...11
UPN and Federated identity added successfully
[2025-04-30T23:50:39.480Z] Executed 'ciamHelper' (Succeeded,
Id=ec...a7, Duration=2788ms)
[2025-04-30T23:50:41.549Z] Executing 'ciamHelper'
(Reason='This function was programmati...4a...df)
C# HTTP trigger function processed a request.
Object Id 02...31 Email Password Method
getPhone Phone number Display name Given name Surname UPN
issuerAssignedId issuer mailNickname
method getPhone
----------------------------------------------
Getting phone number objectId 02...31
[2025-04-30T23:50:45.035Z] Executed 'ciamHelper' (Succeeded,
Id=ad...df, Duration=3718ms)
[2025-04-30T23:51:32.046Z] Executing 'ciamHelper'
(Reason='This function was programmatically called via the host APIs.',
Id=d9...36)
C# HTTP trigger function processed a request.
Object Id 02...31 Email Password Method
setPhone Phone number +64xxxxxx Display name Given name Surname UPN
issuerAssignedId issuer mailNickname
method setPhone
----------------------------------------------
Setting phone number +64xxxxxx objectId
02...31
[2025-04-30T23:51:37.578Z] Executed 'ciamHelper' (Succeeded,
Id=d9...36, Duration=5780ms)
Profile edit
You can read my post here.
Password reset
This uses the embedded password reset flow.
This avoids sending the AADB2C90118 error code to the application.

The “Forgot your password” “link” is a federation button made to appear as a link via the metadata:
ForgotPasswordExchange
Running it:

Enter the email address of the user whose password needs to be reset.
The user then validates the email via the MFA SMS flow as above.

and can now reset their password.
If the user has not proofed up, they get an error message:

The console logging shows:
C# HTTP trigger function processed a request.
Object Id Email HybridCreate8@company.co.nz Password Method read
Phone number Display name Given name Surname UPN issuerAssignedId
issuer mailNickname
method read
----------------------------------------------
Email - Reading user HybridCreate8@company.co.nz
ID: a0...76
Display Name: Hybrid Create8
Given Name: HybridPE
Surname: Create8
Email:
User Principal Name (UPN):
cpim_5e...90@tenant.onmicrosoft.com
--------------------------------------------------
[2025-05-02T02:00:59.505Z] Executed 'ciamHelper'
(Succeeded, Id=47...61, Duration=5509ms)
[2025-05-02T02:01:00.860Z] Executing 'ciamHelper'
(Reason='This function was programmatically called via the host APIs.',
Id=4b...0)
C# HTTP trigger function processed a request.
Object Id a0...96 Email Password Method getPhone
Phone number Display name Given name Surname UPN issuerAssignedId
issuer mailNickname
method getPhone
----------------------------------------------
Getting phone number objectId a0...76
Phone number Microsoft.Graph.Beta.Models.PhoneAuthenticationMethod
[2025-05-02T02:01:07.148Z] Executed 'ciamHelper' (Succeeded,
Id=4b...a0f60, Duration=6518ms)
[2025-05-02T02:03:35.827Z] Executing 'ciamHelper'
(Reason='This function was programmatically called via the host APIs.',
Id=a0...bc)
C# HTTP trigger function processed a request.
Object Id a0...76 Email
Password somepassword Method resetPassword Phone number Display name
Given name Surname UPN issuerAssignedId issuer mailNickname
method resetPassword
----------------------------------------------
Reset password objectId a0...b76 password
somepassword
Password reset successfully
[2025-05-02T02:03:38.385Z] Executed 'ciamHelper'
(Succeeded, Id=a0...bc, Duration=2788ms)
All good!
Integrating the Entra External ID hybrid approach via custom policies with the Azure AD B2C MFA… was originally published in The new control plane on Medium, where people are continuing the conversation by highlighting and responding to this story.