Created
March 30, 2025 22:24
-
-
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<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!#$%&'+^_`{}~-]+ # matches lower or upper case letters, digits, and certain special | |
characters | |
(?:\.[a-zA-Z0-9!#$%&'+^_`{}~-]+)* # 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!#$%&'+^_`{}~-]+(?:\.[a-zA-Z0-9!#$%&'+^_`{}~-]+)*@(?:[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> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
https://medium.com/the-new-control-plane/using-azure-ad-b2c-custom-policies-to-implement-profile-edit-on-entra-external-id-c25cb69fbb27