Automate B2C Deployments with Azure Devops

It can be hard to manage different environments in Azure AD B2C. You need to switch to all the different tenants to upload your policies. This is not an ideal situation when working on multiple customer projects.

This blog describes how to automate your deployments on multiple environments. The configuration on the script is not developed by myself. I’ve reused this script from Daniel Krzyczkowski.

Structure of folders

This is how I structured my folders for the automated deployment. I’ve configured a Policy folder with my main B2C Code and my appsettings.json for environment variables, an Environments folder for DEV and PRD and a scripts folder where I added the script for the automatic deployment in Azure DevOps:

In appsettings.json I’ve configured the parameters that changes on each environment. For example the tenant differs on each environment also the appsettings for B2C are different on each environment:

In my main B2C files (files that are not under DEV or PRD folder) I use the variables with the syntax {settings:nameofparameter}:

If you’ve installed the B2C extensions you can perform CTRL + Shift + P to perform a policy build. This will automatically create or update the files in the DEV and PRD folder (Image 1 policy build, image 2 Trustframeworkbase for PRD after policy build)

Setup Azure DevOps

The first action that you need to perform is to copy your Azure AD B2C tenant ID’s (DEV and PRD environment)

The second step is to create an app registration for your devops release pipeline

Name your app registration, this application is only for internal accounts set the redirect uri to http://localhost

Copy the client ID as you need this in your script

Add the necessary API permissions so that your application can deploy your custom policies

Grant admin consent for the newly added API permissions

The last step is to create a client secret and copy this value as you need it in your script

Create Script

As I mentioned before I used the script of Daniel but modified 1 line in his powershell script to use it in different environments. I’ve modified the script on line 135. I’ve added the $environment parameter and changed it towards my DevOps tenant. I’ve also added an extra parameter on line 8 to request the environment. Save this in the Script folder: 

[CmdletBinding()]
param (
    $AdB2cAutomationAppId,
    $AdB2cAutomationAppSecret,
    $AdB2cAutomationTenantId,
    $CustomPolicyFileName,
    $CustomPolicyName,
    $environment
)

# 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 "_B2C Automation/policies/Environments/$environment/$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))
Write-Host $credential	
$token = Get-MSGraphAuthToken -credential $credential -tenantID $AdB2cAutomationTenantId
Write-Host $token
$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

Create release pipeline

The automation can be performed by creating a release pipeline. Create a new release pipeline in your Azure DevOps tenant

Configure the artifact based on your repository

Create a new stage with an empty job and name it DEV

Create a new stage and name it PRD

The structure will look like this

Create a task group with an PowerShell task for each custom policy file. Please keep in mind the sequence where B2C templates needs to be deployed: Base file, base extensions and afterwards the other relying party files. As script path use the path where you stored your script (Scripts folder). Add following arguments (the example below is for the Base file):

-CustomPolicyFileName $(TrustFrameworkBasePolicyFileName) -CustomPolicyName $(TrustFrameworkBasePolicyName) -AdB2cAutomationAppId $(AdB2cAutomationAppId) -AdB2cAutomationAppSecret $(AdB2cAutomationAppSecret) -AdB2cAutomationTenantId $(AdB2cAutomationTenantId) -environment $(environment)

Go back to the DEV stage and add your Task Group

Fill in the the variable names do the same operation for PRD, use the same variable names for DEV and PRD (note we will implement those variables in the next task)

In the upper left corner go to Variables and create following pipeline variables those are equal on each environment. The variables with PolicyName is how you named your policy in B2C, the variables with PolicyFileName is how you named your file name in your git repo without the extension

The next section is to create a variable group for DEV and a variable group for PRD. Use the following variables: AdB2cAutomationAppId (app id of your release pipeline app), AdB2cAutomationAppSecret (Secret value of your release pipeline app), AdB2cAutomationTenantId (Tenant ID of your B2C environment), enviroment

Go back to your release pipeline and click on variables and link your DEV variable group and link it to the DEV stage. Do the same action for PRD.

Afterwards run your release pipeline to upload your custom policies towards DEV and PRD:

Result on PRD environment

Published by jordyblommaert

My passion is the cloud

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

%d bloggers like this: