Skip to content

Instantly share code, notes, and snippets.

@rbrayb
Created March 30, 2025 22:24
Show Gist options
  • Save rbrayb/5c42354bb6ac59b995d349ce41354110 to your computer and use it in GitHub Desktop.
Save rbrayb/5c42354bb6ac59b995d349ce41354110 to your computer and use it in GitHub Desktop.
Using Azure AD B2C custom policies to implement Profile Edit on Entra External ID
<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="tenant.onmicrosoft.com" PolicyId="B2C_1A_OrchestrateToCiamV2_PE"
PublicPolicyUri="http://tenant.onmicrosoft.com/B2C_1A_OrchestrateToCiamV2_PE"
DeploymentMode="Development"
UserJourneyRecorderEndpoint="urn:journeyrecorder:applicationinsights">
<BasePolicy>
<TenantId>tenant.onmicrosoft.com</TenantId>
<PolicyId>B2C_1A_AUG_MFA_TRUSTFRAMEWORKEXTENSIONS</PolicyId>
</BasePolicy>
<BuildingBlocks>
<ClaimsSchema>
<ClaimType Id="newlyEnrolled">
<DisplayName>newlyEnrolled</DisplayName>
<DataType>string</DataType>
<UserHelpText />
</ClaimType>
<ClaimType Id="graph_bearerToken">
<DisplayName>Bearer token</DisplayName>
<DataType>string</DataType>
</ClaimType>
<ClaimType Id="method">
<DisplayName>api method</DisplayName>
<DataType>string</DataType>
</ClaimType>
<ClaimType Id="ropc_grant_type">
<DisplayName>ropc_grant_type</DisplayName>
<DataType>string</DataType>
<AdminHelpText>ropc_grant_type</AdminHelpText>
<UserHelpText>ropc_grant_type</UserHelpText>
</ClaimType>
</ClaimsSchema>
<Predicates>
<Predicate Id="email" Method="MatchesRegex">
<UserHelpText>Please enter a valid email address.</UserHelpText>
<Parameters>
<!--
This regex is constructed mostly from RFC 5322 for email, with intentional omissions based on
discovery of characters that don't work for other services we use
# the below two lines cover the local part of the email, before the '@' sign
[a-zA-Z0-9!#$%&amp;'+^_`{}~-]+ # matches lower or upper case letters, digits, and certain special
characters
(?:\.[a-zA-Z0-9!#$%&amp;'+^_`{}~-]+)* # same list as above, but including an optional '.' character
at the beginning, repeated
# together, the above two lines prevent the '.' character from appearing at the start, end, or
twice in a row in the local part
@ # the '@' symbol appears exactly once, seperating the local and domain sections
(?:[a-zA-Z0-9] # matches lower and uppercase letters and digits
(?:[a-zA-Z0-9-]* # same as above, but also allowing '-'
[a-zA-Z0-9]) # only lower and uppercase letters and digits again
?\.)+ # allows for a '.' character to terminate a section
# the above lines mean that '.' can create segments, and segments can't begin or end with a '-'.
Also, no repeating '.' chars
[a-zA-Z0-9](?:[a-zA-Z0-9-]*[a-zA-Z0-9])?$
# the above line is the essentially same as the previous section, but forces the email to not end
with a '.'
-->
<Parameter Id="RegularExpression">^[a-zA-Z0-9!#$%&amp;'+^_`{}~-]+(?:\.[a-zA-Z0-9!#$%&amp;'+^_`{}~-]+)*@(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]*[a-zA-Z0-9])?\.)+[a-zA-Z0-9](?:[a-zA-Z0-9-]*[a-zA-Z0-9])?$</Parameter>
</Parameters>
</Predicate>
</Predicates>
</BuildingBlocks>
<ClaimsProviders>
<!-- SUSI/Account Link/ForgotPwd journey forwarder -->
<ClaimsProvider>
<DisplayName>Local account sign up and sign in</DisplayName>
<TechnicalProfiles>
<TechnicalProfile Id="CIAM-SelfAsserted-LocalAccountSignin-Email_PE">
<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.localaccountsignin</Item>
<Item Key="IncludeClaimResolvingInClaimsHandling">true</Item>
</Metadata>
<IncludeInSso>false</IncludeInSso>
<InputClaims>
<InputClaim ClaimTypeReferenceId="signInName" />
</InputClaims>
<OutputClaims>
<OutputClaim ClaimTypeReferenceId="signInName" Required="true" />
<OutputClaim ClaimTypeReferenceId="password" Required="true" />
<OutputClaim ClaimTypeReferenceId="objectId" />
<OutputClaim ClaimTypeReferenceId="authenticationSource"
DefaultValue="localViaCiamTenant" />
<OutputClaim ClaimTypeReferenceId="graph_bearerToken" />
</OutputClaims>
<ValidationTechnicalProfiles>
<ValidationTechnicalProfile ReferenceId="REST-login-NonInteractive-CIAM_PE" />
<ValidationTechnicalProfile ReferenceId="REST-fetchUserProfile-CIAM_PE" />
</ValidationTechnicalProfiles>
<UseTechnicalProfileForSessionManagement ReferenceId="SM-AAD" />
</TechnicalProfile>
<TechnicalProfile Id="CIAM_SelfAsserted-ProfileUpdate">
<DisplayName>User ID update</DisplayName>
<Protocol Name="Proprietary"
Handler="Web.TPEngine.Providers.SelfAssertedAttributeProvider, Web.TPEngine, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" />
<Metadata>
<Item Key="ContentDefinitionReferenceId">api.selfasserted.profileupdate</Item>
<!-- <Item Key="ContentDefinitionReferenceId">api.selfasserted</Item> -->
</Metadata>
<IncludeInSso>false</IncludeInSso>
<InputClaims>
<InputClaim ClaimTypeReferenceId="alternativeSecurityId" />
<InputClaim ClaimTypeReferenceId="userPrincipalName" />
<InputClaim ClaimTypeReferenceId="displayName" />
<InputClaim ClaimTypeReferenceId="givenName" />
<InputClaim ClaimTypeReferenceId="surname" />
</InputClaims>
<OutputClaims>
<!-- Required claims -->
<OutputClaim ClaimTypeReferenceId="executed-SelfAsserted-Input"
DefaultValue="true" />
<OutputClaim ClaimTypeReferenceId="displayName" />
<OutputClaim ClaimTypeReferenceId="givenName" />
<OutputClaim ClaimTypeReferenceId="surname" />
</OutputClaims>
</TechnicalProfile>
</TechnicalProfiles>
</ClaimsProvider>
<ClaimsProvider>
<DisplayName>Local Account Sign Up and Sign in APIs</DisplayName>
<TechnicalProfiles>
<TechnicalProfile Id="REST-CIAM-UserUpdateUsingLogonEmail">
<DisplayName>Update user in CIAM tenant</DisplayName>
<Protocol Name="Proprietary"
Handler="Web.TPEngine.Providers.RestfulProvider, Web.TPEngine, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" />
<Metadata>
<!-- Set the ServiceUrl with your own REST API endpoint -->
<Item Key="ServiceUrl">https://83a6-222-152-99-121.ngrok-free.app/api/ciamhelper</Item>
<Item Key="SendClaimsIn">Body</Item>
<!-- Set AuthenticationType to Basic or ClientCertificate in production
environments -->
<Item Key="AuthenticationType">None</Item>
<!-- REMOVE the following line in production environments -->
<Item Key="AllowInsecureAuthInProduction">true</Item>
<Item Key="DefaultUserMessageIfRequestFailed">REST error</Item>
</Metadata>
<InputClaims>
<!-- Claims sent to your REST API -->
<InputClaim ClaimTypeReferenceId="email" />
<!-- <InputClaim ClaimTypeReferenceId="newPassword"
PartnerClaimType="password" /> -->
<InputClaim ClaimTypeReferenceId="objectId" />
<InputClaim ClaimTypeReferenceId="displayName" />
<InputClaim ClaimTypeReferenceId="givenName" />
<InputClaim ClaimTypeReferenceId="surName" />
<InputClaim ClaimTypeReferenceId="userPrincipalName" />
<InputClaim ClaimTypeReferenceId="method" AlwaysUseDefaultValue="true"
DefaultValue="update" />
</InputClaims>
<OutputClaims>
<!-- Claims parsed from your REST API -->
<OutputClaim ClaimTypeReferenceId="objectId" PartnerClaimType="id" />
<OutputClaim ClaimTypeReferenceId="displayName" />
<OutputClaim ClaimTypeReferenceId="givenName" />
<OutputClaim ClaimTypeReferenceId="surName" />
</OutputClaims>
<UseTechnicalProfileForSessionManagement ReferenceId="SM-Noop" />
</TechnicalProfile>
<TechnicalProfile Id="CIAM-UserReadUsingObjectId_PE">
<DisplayName>Write user into CIAM tenant</DisplayName>
<Protocol Name="Proprietary"
Handler="Web.TPEngine.Providers.RestfulProvider, Web.TPEngine, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" />
<Metadata>
<!-- Set the ServiceUrl with your own REST API endpoint -->
<Item Key="ServiceUrl">https://83a6-222-152-99-121.ngrok-free.app/api/ciamhelper</Item>
<Item Key="SendClaimsIn">Body</Item>
<!-- Set AuthenticationType to Basic or ClientCertificate in production
environments -->
<Item Key="AuthenticationType">None</Item>
<!-- REMOVE the following line in production environments -->
<Item Key="AllowInsecureAuthInProduction">true</Item>
</Metadata>
<InputClaims>
<!-- Claims sent to your REST API -->
<InputClaim ClaimTypeReferenceId="objectId" />
<InputClaim ClaimTypeReferenceId="method" AlwaysUseDefaultValue="true"
DefaultValue="read" />
</InputClaims>
<OutputClaims>
<!-- Claims parsed from your REST API -->
<OutputClaim ClaimTypeReferenceId="signInNames.emailAddress"
PartnerClaimType="identities.0.issuerAssignedId" />
<OutputClaim ClaimTypeReferenceId="displayName"
PartnerClaimType="displayName" />
<OutputClaim ClaimTypeReferenceId="otherMails" PartnerClaimType="otherMails" />
<OutputClaim ClaimTypeReferenceId="givenName" PartnerClaimType="givenName" />
<OutputClaim ClaimTypeReferenceId="surname" PartnerClaimType="surname" />
<OutputClaim ClaimTypeReferenceId="userPrincipalName"
PartnerClaimType="userPrincipalName" />
</OutputClaims>
<UseTechnicalProfileForSessionManagement ReferenceId="SM-Noop" />
</TechnicalProfile>
<TechnicalProfile Id="REST-login-NonInteractive-CIAM_PE">
<DisplayName>non interactive authentication to APAC</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://83a6-222-152-99-121.ngrok-free.app/api/ciamhelper</Item>
<Item Key="AuthenticationType">None</Item>
<Item Key="SendClaimsIn">Body</Item>
<Item Key="AllowInsecureAuthInProduction">true</Item>
</Metadata>
<InputClaims>
<InputClaim ClaimTypeReferenceId="signInName" PartnerClaimType="email" />
<InputClaim ClaimTypeReferenceId="password" />
<InputClaim ClaimTypeReferenceId="method" DefaultValue="auth"
AlwaysUseDefaultValue="true" />
</InputClaims>
<OutputClaims>
<OutputClaim ClaimTypeReferenceId="graph_bearerToken"
PartnerClaimType="access_token" />
</OutputClaims>
<UseTechnicalProfileForSessionManagement ReferenceId="SM-Noop" />
</TechnicalProfile>
<TechnicalProfile Id="REST-fetchUserProfile-CIAM_PE">
<DisplayName>fetch user profile cross tenant</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://graph.microsoft.com/beta/me</Item>
<Item Key="AuthenticationType">Bearer</Item>
<Item Key="UseClaimAsBearerToken">graph_bearerToken</Item>
<Item Key="SendClaimsIn">Url</Item>
</Metadata>
<InputClaims>
<InputClaim ClaimTypeReferenceId="graph_bearerToken" />
</InputClaims>
<OutputClaims>
<OutputClaim ClaimTypeReferenceId="objectId" PartnerClaimType="id" />
<OutputClaim ClaimTypeReferenceId="givenName" />
<OutputClaim ClaimTypeReferenceId="surName" />
<OutputClaim ClaimTypeReferenceId="displayName" />
<OutputClaim ClaimTypeReferenceId="userPrincipalName" PartnerClaimType="upn" />
<OutputClaim ClaimTypeReferenceId="authenticationSource"
DefaultValue="localAccountAuthentication" />
</OutputClaims>
<UseTechnicalProfileForSessionManagement ReferenceId="SM-Noop" />
</TechnicalProfile>
</TechnicalProfiles>
</ClaimsProvider>
</ClaimsProviders>
<UserJourneys>
<UserJourney Id="ProfileEdit_CIAM">
<OrchestrationSteps>
<OrchestrationStep Order="1" Type="ClaimsProviderSelection"
ContentDefinitionReferenceId="api.idpselections">
<ClaimsProviderSelections>
<ClaimsProviderSelection
TargetClaimsExchangeId="LocalAccountSigninEmailExchange" />
</ClaimsProviderSelections>
</OrchestrationStep>
<OrchestrationStep Order="2" Type="ClaimsExchange">
<ClaimsExchanges>
<ClaimsExchange Id="LocalAccountSigninEmailExchange"
TechnicalProfileReferenceId="CIAM-SelfAsserted-LocalAccountSignin-Email_PE" />
</ClaimsExchanges>
</OrchestrationStep>
<OrchestrationStep Order="3" Type="ClaimsExchange">
<ClaimsExchanges>
<ClaimsExchange Id="AADUserReadWithObjectId"
TechnicalProfileReferenceId="CIAM-UserReadUsingObjectId_PE" />
</ClaimsExchanges>
</OrchestrationStep>
<OrchestrationStep Order="4" Type="ClaimsExchange">
<ClaimsExchanges>
<ClaimsExchange Id="B2CUserProfileUpdateExchange"
TechnicalProfileReferenceId="CIAM_SelfAsserted-ProfileUpdate" />
</ClaimsExchanges>
</OrchestrationStep>
<OrchestrationStep Order="5" Type="ClaimsExchange">
<ClaimsExchanges>
<ClaimsExchange Id="B2CUserProfileRESTUpdateExchange"
TechnicalProfileReferenceId="REST-CIAM-UserUpdateUsingLogonEmail" />
</ClaimsExchanges>
</OrchestrationStep>
<OrchestrationStep Order="6" Type="SendClaims"
CpimIssuerTechnicalProfileReferenceId="JwtIssuer" />
</OrchestrationSteps>
<ClientDefinition ReferenceId="DefaultWeb" />
</UserJourney>
</UserJourneys>
<RelyingParty>
<DefaultUserJourney ReferenceId="ProfileEdit_CIAM" />
<UserJourneyBehaviors>
<JourneyInsights TelemetryEngine="ApplicationInsights"
InstrumentationKey="123456" 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="userPrincipalName" />
<OutputClaim ClaimTypeReferenceId="signInName" />
<OutputClaim ClaimTypeReferenceId="objectId" PartnerClaimType="sub" />
<OutputClaim ClaimTypeReferenceId="tenantId" AlwaysUseDefaultValue="true"
DefaultValue="{Policy:TenantObjectId}" />
</OutputClaims>
<SubjectNamingInfo ClaimType="sub" />
</TechnicalProfile>
</RelyingParty>
</TrustFrameworkPolicy>
using System;
using System.IO;
using System.Threading.Tasks;
using System.Text;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.Http;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Azure.Identity;
using Microsoft.Graph.Beta;
using Microsoft.Graph.Beta.Models;
using System.Collections.Generic;
using System.Net;
using System.Reflection;
using System.Net.Http;
namespace readUser
{
public static class ciamHelper
{
[FunctionName("ciamHelper")]
public static async Task<IActionResult> Run(
[HttpTrigger(AuthorizationLevel.Function, "get", "post", Route = null)] HttpRequest req,
ILogger log)
{
//log.LogInformation("C# HTTP trigger function processed a request.");
Console.WriteLine("\n" + "C# HTTP trigger function processed a request.");
string requestBody = await new StreamReader(req.Body).ReadToEndAsync();
dynamic data = JsonConvert.DeserializeObject(requestBody);
string objectId = data.objectId;
string email = data.email;
string password = data.password;
string method = data.method;
string phoneNumber = data.phoneNumber;
string displayName = data.displayName;
string givenName = data.givenName;
string surName = data.surName;
string upn = data.upn;
Console.WriteLine("\n" + "Object Id " + objectId + " Email " + email + " Password " + password + " Method " + method
+ " Phone number " + phoneNumber + " Display name " + displayName + " Given name " + givenName + " Surname " + surName
+ " UPN " + upn );
// TODO: Add Entra External IDP tenant ID
var tenantId = "externaltenantobjectId";
if (method == "auth")
{
Console.WriteLine("\n" + "Authenticating user");
using (var httpClient = new HttpClient())
{
// Build the request URL
// TODO : Add Entra External IDP tenant name
//var requestUrl = "https://externaltenant.ciamlogin.com/externaltenantobjectId/oauth2/token";
// TODO: Add Entra External IDP tenant name and ID
var requestUrl = "https://externaltenant.ciamlogin.com/externaltenantobjectId/oauth2/v2.0/token";
//string auth_resource = "https://graph.microsoft.com"; // Replace with your specific resource URL
string scope = "https://graph.microsoft.com/.default";
// TODO: Add RopcFromB2C client ID
string auth_clientId = "clientId";
// Prepare the request body
//var auth_requestBody = $"resource={auth_resource}&client_id={auth_clientId}&grant_type=password&username={email}&password={password}&nca=1";
var auth_requestBody = $"scope={scope}&client_id={auth_clientId}&grant_type=password&username={email}&password={password}&nca=1";
// Convert the request body to a byte array
var content = new StringContent(auth_requestBody, Encoding.UTF8, "application/x-www-form-urlencoded");
Console.WriteLine("\n" + "Request URL " + requestUrl);
Console.WriteLine("\n" + "Body " + auth_requestBody);
// Send the POST request to Azure AD
using (var response = await httpClient.PostAsync(requestUrl, content))
{
// Check if the request was successful
if (response.IsSuccessStatusCode)
{
// Read the response content
var responseContent = await response.Content.ReadAsStringAsync();
var jsonObj = JsonConvert.DeserializeObject(responseContent);
Console.WriteLine("\n" + "jsonObject " + jsonObj.ToString());
return new OkObjectResult(jsonObj);
}
else
{
// Handle error cases here
// For example, log the error or throw an exception
return new ConflictObjectResult(new B2CResponseModel($"Invalid username or password.", HttpStatusCode.Conflict));
}
}
}
}
else
{
Console.WriteLine("\n" + "method " + method);
var scopes = new[] { "https://graph.microsoft.com/.default" };
// Values from app registration
// TODO: Add GraphCallsFromB2CTenant client ID and secret
var clientId = "clientId";
var clientSecret = "clientSecret";
// using Azure.Identity;
var options = new TokenCredentialOptions
{
AuthorityHost = AzureAuthorityHosts.AzurePublicCloud
};
// https://learn.microsoft.com/dotnet/api/azure.identity.clientsecretcredential
var clientSecretCredential = new ClientSecretCredential(
tenantId, clientId, clientSecret, options);
// get accessToken
var accessToken = await clientSecretCredential.GetTokenAsync(new Azure.Core.TokenRequestContext(scopes) { });
Console.WriteLine("\n" + accessToken.Token);
var graphClient = new GraphServiceClient(clientSecretCredential, scopes);
if (objectId != null && method == "read")
{
Console.WriteLine("\n" + "ObjectId - Reading user " + objectId);
var user = await graphClient.Users[objectId].GetAsync();
//log.LogInformation(user.ToString());
var logGivenName = user.GivenName;
var logSurName = user.Surname;
var logDisplayName = user.DisplayName;
var logEmail = user.Identities[0].IssuerAssignedId;
var logPhoneNumber = user.MobilePhone;
var logUPN = user.UserPrincipalName;
Console.WriteLine("\n" + "Given name " + logGivenName + " Surname " + logSurName + " Display name " + logDisplayName);
Console.WriteLine("Email " + logEmail + " Phone number " + logPhoneNumber + " UPN " + logUPN);
return new OkObjectResult(user);
}
if (email != null && method == "read")
{
Console.WriteLine("\n" + "Email - Reading user " + email);
var user = await graphClient.Users.GetAsync((requestConfiguration) =>
{
// TODO : Add Entra External IDP tenant name
requestConfiguration.QueryParameters.Filter = string.Format("identities/any(x:x/issuerAssignedId eq '{0}' " +
"and x/issuer eq 'externaltenant.onmicrosoft.com') ", email);
});
Console.WriteLine("User details " + user.ToString());
return new OkObjectResult(user);
}
if (method == "createUser")
{
Console.WriteLine("\n" + "Creating user");
Console.WriteLine("Display name " + displayName + " email " + email);
var userRequestBody = new User
{
AccountEnabled = true,
DisplayName = displayName,
GivenName = givenName,
Surname = surName,
UserPrincipalName = upn,
Identities = new List<ObjectIdentity>
{
new ObjectIdentity
{
SignInType = "emailAddress",
// TODO: Add Entra External IDP tenant name
Issuer = "externaltenant.onmicrosoft.com",
IssuerAssignedId = email,
}
},
PasswordProfile = new PasswordProfile
{
Password = password,
ForceChangePasswordNextSignIn = false,
},
PasswordPolicies = "DisablePasswordExpiration",
};
try
{
var result = await graphClient.Users.PostAsync(userRequestBody);
string stringObjectId = result.Id;
try
{
await DoWithRetryAsync(TimeSpan.FromSeconds(1), tryCount: 10, stringObjectId, email, graphClient);
}
catch (Exception enrolEx)
{
return new ConflictObjectResult(enrolEx);
}
return new OkObjectResult(result);
}
catch (Exception ex)
{
Console.WriteLine("\n" + "Exception " + ex);
Console.WriteLine("\n" + "Error creating user - account already exists ");
return new ConflictObjectResult(new B2CResponseModel($"This account already exists.", HttpStatusCode.Conflict));
}
}
if (method == "getPhone")
{
Console.WriteLine("\n" + "Getting phone number");
try
{
var result = await graphClient.Users[objectId].Authentication.PhoneMethods.GetAsync();
return new OkObjectResult(result.Value[0]);
}
catch (Exception exception)
{
Console.WriteLine("\n" + "Exception " + exception);
Console.WriteLine("\n" + "Adding phone number");
var jsonObject = new JObject();
jsonObject.Add("phoneNumber", "null");
return new OkObjectResult(jsonObject);
}
}
if (method == "setPhone")
{
Console.WriteLine("\n" + "Setting phone number " + phoneNumber);
var mfaRequestBody = new PhoneAuthenticationMethod
{
PhoneNumber = phoneNumber,
PhoneType = AuthenticationPhoneType.Mobile,
};
var enrolResult = await graphClient.Users[objectId].Authentication.PhoneMethods.PostAsync(mfaRequestBody);
return new OkObjectResult(enrolResult);
}
if (method == "update")
{
Console.WriteLine("\n" + "Updating user");
Console.WriteLine("Object Id " + objectId + " Display name " + displayName + " Given name " + givenName +
" Surname " + surName + " UPN " + upn + " Email " + email);
var userUpdateBody = new User
{
DisplayName = displayName,
GivenName = givenName,
Surname = surName,
//UserPrincipalName = upn,
//Identities = new List<ObjectIdentity>
//{
// new ObjectIdentity
// {
// SignInType = "emailAddress",
// Issuer = "externaltenant.onmicrosoft.com",
// IssuerAssignedId = email,
// }
//}
};
try
{
await graphClient.Users[objectId].PatchAsync(userUpdateBody);
var updatedUser = await graphClient.Users[objectId].GetAsync();
return new OkObjectResult(updatedUser);
}
catch (Exception ex)
{
Console.WriteLine("\n" + "Exception " + ex);
Console.WriteLine("\n" + "Error updating user");
return new ConflictObjectResult(new B2CResponseModel($"Error updating user.", HttpStatusCode.Conflict));
}
}
}
return new OkObjectResult(null);
}
public static async Task EnrolEmail(GraphServiceClient graphClient, string email, string objectId)
{
Console.WriteLine("\n" + "Enrolling email address:" + " email " + email + " objectId " + objectId);
var emailAuthMethodRequestBody = new EmailAuthenticationMethod
{
EmailAddress = email
};
var result = await graphClient.Users[objectId].Authentication.EmailMethods.PostAsync(emailAuthMethodRequestBody);
Console.WriteLine("\n" + "result " + result);
//return new OkObjectResult(enrolResult);
}
//public static async Task DoWithRetryAsync(TimeSpan sleepPeriod, int tryCount = 3, string objectId="test", string email="test", GraphServiceClient graphClient=null)
public static async Task DoWithRetryAsync(TimeSpan sleepPeriod, int tryCount, string objectId, string email,
GraphServiceClient graphClient)
{
Console.WriteLine("\n" + "DoWithRetryAsync");
Console.WriteLine("\n" + "objectId " + objectId + " email " + email);
if (tryCount <= 0)
throw new ArgumentOutOfRangeException(nameof(tryCount));
while (true)
{
try
{
await EnrolEmail(graphClient, email, objectId);
return;
}
catch
{
if (--tryCount == 0)
throw;
await Task.Delay(sleepPeriod);
}
}
}
}
public class B2CResponseModel
{
public string version { get; set; }
public int status { get; set; }
public string userMessage { get; set; }
public B2CResponseModel(string message, HttpStatusCode status)
{
Console.WriteLine("\n" + "B2C response " + message);
this.userMessage = message;
this.status = (int)status;
this.version = Assembly.GetExecutingAssembly().GetName().Version.ToString();
}
}
}