Azure AD B2C Series - Custom Policies release automation with Azure DevOps
I had a chance to work with the Azure Active Directory B2C quite a lot recently and decided that it would be nice to share some knowledge about it. Just to make life easier for people using it especially when there are some custom usage scenarios. This is the fourth article from the series and in this article I would like to present how to setup release pipeline in Azure DevOps to automatically update custom policies in the Azure AD B2C Identity Experience Framework.
There are also links to the great content after opening “Identity Experience Framework” tab in the Azure portal:
Introduction and assumptions
Before we start I assume that you are familiar with the Azure AD B2C Identity Experience Framework and initial setup is done. If you would like to start I recommend to check official documentation. In this series we will use already configured AD B2C tenant with Custom Policies. After all steps from the documentation are completed we are ready to move forward.
Prepare Azure AD B2C for automatic updates of custom policies
We are going to access and update custom policies using Microsoft Graph API. To be able to do it we need to register new application in the Azure Active Directory (in Azure AD B2C tenant) and assign the right permissions. Let’s see how to do it.
Register new application in the Azure Active Directory
In the tenant of your Azure AD B2C in the Azure portal select “Azure Active Directory” tab:
Check Tenant ID - we will need it to setup release pipeline in the Azure DevOps:
Select “App registrations (Legacy) and click “New application registration”:
Provide the name for the app and set fields as presented below:
Once app is created, copy “Application ID” - we will need it to setup release pipeline in the Azure DevOps:
Then open “Required permissions” tab and click “Add” button:
Select “Microsoft Graph” and select checkbox under “Application Permissions” called “Read and write your organization’s trust framework policies”:
Then click “Grant permissions” button:
Now open “Keys” tab and generate new key as shown below. Copy its value - we will need it to setup release pipeline in the Azure DevOps:
Prepare GIT repository with policies files and script files
We will keep custom policies xml files in the GIT repository together with script I described below:
Update-ADB2cPolicies.ps1 script is responsible for obtaining access token which is used to access Microsoft Graph API. Then script updates custom policies files in the Azure AD B2C tenant with obtained access token.
Update-ADB2cPolicies.ps1 script looks like below.
Important
I modified the script which was originally published on this website.
Without this great content I would not be able to achieve policies automation.
Here I would like to also say thank you to Kacper Mucha who helped me a lot with script modifications.
[CmdletBinding()]
param (
$AdB2cAutomationAppId,
$AdB2cAutomationAppSecret,
$AdB2cAutomationTenantId,
$CustomPolicyFileName,
$CustomPolicyName
)
# Functions used to call Microsoft Graph API:
Function Get-MSGraphAuthToken{
[cmdletbinding()]
Param(
[parameter(Mandatory=$true)]
[pscredential]$credential,
[parameter(Mandatory=$true)]
[string]$tenantID
)
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
#Get token
$AuthUri = "https://login.microsoftonline.com/$TenantID/oauth2/token"
$Resource = 'graph.microsoft.com'
$AuthBody = "grant_type=client_credentials&client_id=$($credential.UserName)&client_secret=$($credential.GetNetworkCredential().Password)&resource=https%3A%2F%2F$Resource%2F"
$Response = Invoke-RestMethod -Method Post -Uri $AuthUri -Body $AuthBody
If($Response.access_token){
return $Response.access_token
}
Else{
Throw "Authentication failed"
}
}
Function Invoke-MSGraphQuery{
[CmdletBinding(DefaultParametersetname="Default")]
Param(
[Parameter(Mandatory=$true,ParameterSetName='Default')]
[Parameter(Mandatory=$true,ParameterSetName='Refresh')]
[string]$URI,
[Parameter(Mandatory=$false,ParameterSetName='Default')]
[Parameter(Mandatory=$false,ParameterSetName='Refresh')]
[string]$Body,
[Parameter(Mandatory=$true,ParameterSetName='Default')]
[Parameter(Mandatory=$true,ParameterSetName='Refresh')]
[string]$token,
[Parameter(Mandatory=$false,ParameterSetName='Default')]
[Parameter(Mandatory=$false,ParameterSetName='Refresh')]
[ValidateSet('GET','POST','PUT','PATCH','DELETE')]
[string]$method = "GET",
[Parameter(Mandatory=$false,ParameterSetName='Default')]
[Parameter(Mandatory=$false,ParameterSetName='Refresh')]
[switch]$recursive,
[Parameter(Mandatory=$true,ParameterSetName='Refresh')]
[switch]$tokenrefresh,
[Parameter(Mandatory=$true,ParameterSetName='Refresh')]
[pscredential]$credential,
[Parameter(Mandatory=$true,ParameterSetName='Refresh')]
[string]$tenantID
)
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
$authHeader = @{
'Accept'= 'application/xml'
'Content-Type'= 'application/xml'
'Authorization'= $Token
}
[array]$returnvalue = $()
Try{
If($body){
$Response = Invoke-RestMethod -Uri $URI -Headers $authHeader -Body $Body -Method $method -ErrorAction Stop
}
Else{
$Response = Invoke-RestMethod -Uri $URI -Headers $authHeader -Method $method -ErrorAction Stop
}
}
Catch{
If(($Error[0].ErrorDetails.Message | ConvertFrom-Json -ErrorAction SilentlyContinue).error.Message -eq 'Access token has expired.' -and $tokenrefresh){
$token = Get-MSGraphAuthToken -credential $credential -tenantID $TenantID
$authHeader = @{
'Accept'= 'application/xml'
'Content-Type'= 'application/xml'
'Authorization'=$Token
}
$returnvalue = $()
If($body){
$Response = Invoke-RestMethod -Uri $URI –Headers $authHeader -Body $Body –Method $method -ErrorAction Stop
}
Else{
$Response = Invoke-RestMethod -Uri $uri –Headers $authHeader –Method $method
}
}
Else{
Throw $_
}
}
$returnvalue += $Response
If(-not $recursive -and $Response.'@odata.nextLink'){
Write-Warning "Query contains more data, use recursive to get all!"
Start-Sleep 1
}
ElseIf($recursive){
If($PSCmdlet.ParameterSetName -eq 'default'){
If($body){
$returnvalue += Invoke-MSGraphQuery -URI $Response.'@odata.nextLink' -token $token -body $body -method $method -recursive -ErrorAction SilentlyContinue
}
Else{
$returnvalue += Invoke-MSGraphQuery -URI $Response.'@odata.nextLink' -token $token -method $method -recursive -ErrorAction SilentlyContinue
}
}
Else{
If($body){
$returnvalue += Invoke-MSGraphQuery -URI $Response.'@odata.nextLink' -token $token -body $body -method $method -recursive -tokenrefresh -credential $credential -tenantID $TenantID -ErrorAction SilentlyContinue
}
Else{
$returnvalue += Invoke-MSGraphQuery -URI $Response.'@odata.nextLink' -token $token -method $method -recursive -tokenrefresh -credential $credential -tenantID $TenantID -ErrorAction SilentlyContinue
}
}
}
Return $returnvalue
}
# Get custom policy file content
$customPolicyFileContent = Get-Content -Path "_ad-b2c-policies/policies/$CustomPolicyFileName.xml" -Raw
Write-Host $customPolicyFileContent
# Upload custom policy file to the Azure AD B2C:
$credential = New-Object System.Management.Automation.PSCredential($AdB2cAutomationAppId,(ConvertTo-SecureString $AdB2cAutomationAppSecret -AsPlainText -Force))
$token = Get-MSGraphAuthToken -credential $credential -tenantID $AdB2cAutomationTenantId
$URI = "https://graph.microsoft.com/beta/trustFramework/policies/B2C_1A_$CustomPolicyName/" + '$value'
Write-Host $URI
Invoke-MSGraphQuery -method PUT -URI $URI -Body $customPolicyFileContent -token $token
Important
Please note that I am using beta endpoint for the policies updates using Microsoft Graph: https://graph.microsoft.com/beta
This is because at the moment of writing this article endpoints to update policies with Graph API are in beta.
Prepare release pipeline in Azure DevOps
From the “Pipelines” select “Release” and then click “New pipeline” button and select “Empty job”:
Type the name of the stage - in this case “Dev”:
Configure artifacts by clicking “Add” button:
Select GIT repo as source, then select project, repository and branch like presented below, then click “Add” button
Now select jobs section on the “Dev” stage:
Set job configuration as presented below:
Now add “PowerShell” tasks for each of custom policy file deploy.
Please note that in each task we have to set path to the PowerShell script located in the repository:
$(System.DefaultWorkingDirectory)/_ad-b2c-policies/scripts/Update-ADB2cPolicies.ps1
We need to also pass arguments to the script:
- CustomPolicyFileName - name of the policy file
- CustomPolicyName - name of the policy
- AdB2cAutomationAppId - app ID from the Azure portal
- AdB2cAutomationAppSecret - app secret from the Azure portal (key)
- AdB2cAutomationTenantId - tenant ID from the Azure portal
Below I present arguments for each task:
-CustomPolicyFileName $(TrustFrameworkBasePolicyFileName) -CustomPolicyName $(TrustFrameworkBasePolicyName) -AdB2cAutomationAppId $(AdB2cAutomationAppId) -AdB2cAutomationAppSecret $(AdB2cAutomationAppSecret) -AdB2cAutomationTenantId $(AdB2cAutomationTenantId)
-CustomPolicyFileName $(TrustFrameworkExtensionsPolicyFileName) -CustomPolicyName $(TrustFrameworkExtensionsPolicyName) -AdB2cAutomationAppId $(AdB2cAutomationAppId) -AdB2cAutomationAppSecret $(AdB2cAutomationAppSecret) -AdB2cAutomationTenantId $(AdB2cAutomationTenantId)
-CustomPolicyFileName $(SignUpOrSignInPolicyFileName) -CustomPolicyName $(SignUpOrSignInPolicyName) -AdB2cAutomationAppId $(AdB2cAutomationAppId) -AdB2cAutomationAppSecret $(AdB2cAutomationAppSecret) -AdB2cAutomationTenantId $(AdB2cAutomationTenantId)
-CustomPolicyFileName $(ProfileEditPolicyFileName) -CustomPolicyName $(ProfileEditPolicyName) -AdB2cAutomationAppId $(AdB2cAutomationAppId) -AdB2cAutomationAppSecret $(AdB2cAutomationAppSecret) -AdB2cAutomationTenantId $(AdB2cAutomationTenantId)
-CustomPolicyFileName $(PasswordResetPolicyFileName) -CustomPolicyName $(PasswordResetPolicyName) -AdB2cAutomationAppId $(AdB2cAutomationAppId) -AdB2cAutomationAppSecret $(AdB2cAutomationAppSecret) -AdB2cAutomationTenantId $(AdB2cAutomationTenantId)
Add variables that will be injected as arguments to the script
Now we have to add variables that will be used in the automation script:
Variable name | Variable value |
---|---|
AdB2cAutomationAppId | *** |
AdB2cAutomationAppSecret | *** |
AdB2cAutomationTenantId | *** |
PasswordResetPolicyFileName | PasswordReset |
PasswordResetPolicyName | PasswordReset |
ProfileEditPolicyFileName | ProfileEdit |
ProfileEditPolicyName | ProfileEdit |
SignUpOrSignInPolicyFileName | signup_signin |
SignUpOrSignInPolicyName | signup_signin |
TrustFrameworkBasePolicyFileName | TrustFrameworkBase |
TrustFrameworkBasePolicyName | TrustFrameworkBase |
TrustFrameworkExtensionsPolicyFileName | TrustFrameworkExtensions |
TrustFrameworkExtensionsPolicyName | TrustFrameworkExtensions |
Test release pipeline
Now it is time to test the release pipeline. First remove all policies in the AD B2C tab in the Azure portal (of course I assume you have them in the GIT repository):
Create a new release then:
Wait and see the release pipeline status:
Verify policies in the Azure portal. They should be deployed automatically:
Summary
In this article I presented how setup release pipeline in Azure DevOps to automatically update custom policies in the Azure AD B2C Identity Experience Framework.