Introduction

In today’s complex cloud environments, managing password security across multiple tenants is a critical challenge for IT administrators. Microsoft Entra ID provides powerful mechanisms to implement centralized password policies, but effective implementation requires careful planning and robust automation.

Graph API

Microsoft Graph API revolutionizes Entra ID tenant management by providing a powerful automation framework that simplifies complex multi-tenant configurations. With this API, you can effortlessly streamline identity and security management through comprehensive bulk operations, including:

  • Automated user creation
  • Efficient group management
  • Privileged Identity Management (PIM) role assignments
  • Granular password policy enforcement

The API’s finely tuned permission model lets you stay in control with precision, ensuring every automation script aligns with the principle of least privilege. This not only keeps your operations smooth but also fortifies your identity infrastructure’s security. By embracing Microsoft Graph API, you can shift from time-consuming manual processes to efficient, repeatable workflows that boost consistency and cut down on administrative overhead. Sounds like a win, right? Let’s explore how to make it happen.

The requirements

Before diving into the implementation, ensure you have:

  • An IDE (such as Visual Studio Code)
  • Entra ID Premium license
  • Azure DevOps access with sufficient permissions
  • Service connection(s) that have the Authentication Policy Administrator permission
  • Proficiency in PowerShell
    • Azure PowerShell module
    • Microsoft Graph module

With these prerequisites ready, you’re all set to start building automated solutions that simplify your tenant management.

Scope

Today we will focus on the following subjects:

  • Centralizing banned password management
  • Supporting multi-tenant password restrictions
  • Automating policy deployment through Azure DevOps

To prevent the blog from becoming too long, a few topics are out-of-scope:

  • Multi-tenant deployment strategy
  • Setting up Service Connections in Azure DevOps
  • Creating branch policies in Azure DevOps
  • Various testing/error handling

Configuration steps

Before we dive into the code, I will summarize the order what we will do:

  1. Create/prepare the required payload (body) files for the API
  2. Draft a PowerShell script to update the settings in Entra ID
  3. Create a YAML-pipeline for Azure DevOps that runs automatically

In the end you will have a basic automated way to update Banned Password Lists in multiple Entra ID tenants!

Code Implementation

The code for this blog is hosted on my public Github repository

Folder structure

To be able to create multi-tenant deployments, we are going to parameterize the banned password list settings per tenant. Therefore, we need a consistent folder structure to support this type of deployment.

1
2
3
4
5
6
7
8
9
bannedPasswords
├── code
│   ├── Set-PasswordSettings.ps1
├── parameters
│   ├── passwordSettings.json
│   ├── bannedPasswords-tenantA.json
│   └── bannedPasswords-tenantB.json
├── pipelines
│   ├── set-password-settings.yaml

Uri background

To set the banned password list, we need to update the Entra ID setting ‘Password Rule Settings’. This one is currently in beta only, so as always be aware things may change in the future. See the docs for more information

The uri is:

1
URI https://graph.microsoft.com/beta/settings

Getting the settings returns the following setting types:

1
2
3
4
5
6
7
8
9
id          : xxxxxxxx-d947-4d19-a028-xxxxxxxxxxxx
displayName : Group.Unified
templateId  : 62375ab9-6b52-47ed-826b-58e47e0e304b
values      : {…}

id          : xxxxxxxx-7701-4e25-8c81-xxxxxxxxxxxx
displayName : Password Rule Settings
templateId  : 5cf42378-d67d-4f36-ba46-e8b86229381d
values      : {…}

The API Uri targets ‘settings’, which is manages multiples EntraID settings. Therefore, we target ‘Password Rule Settings’ to validate we use the correct template ID to use in our Uri. This will become clear in the PowerShell code snippet below.

Create/prepare the required payload (body) files for the API

The Password Rule Settings expects a JSON-file body (passwordSettings.json) that contains the settings to update. The file has the following structure:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
{
    "templateId": "5cf42378-d67d-4f36-ba46-e8b86229381d",
    "values": [
        {
            "name": "BannedPasswordCheckOnPremisesMode",
            "value": "Enforce"
        },
        {
            "name": "EnableBannedPasswordCheckOnPremises",
            "value": "True"
        },
        {
            "name": "EnableBannedPasswordCheck",
            "value": "True"
        },
        {
            "name": "LockoutDurationInSeconds",
            "value": "60"
        },
        {
            "name": "LockoutThreshold",
            "value": "10"
        },
        {
            "name": "BannedPasswordList",
            "value": "placeholder"
        }
    ]
}

You may be thinking: we don’t I add all the banned passwords in this file? A valid point, however we want to support multiple tenants we the ability to differentiate. Therefore, I chose to use separate files containing the banned passwords for each tenant.

Banned passwords

Each tenant will have a JSON-file containing the list of banned passwords. The file will be merged with the main parameter file and the merged file will be added as body to the request.

bannedPasswords-tenantA.json:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
[
    "secret",
    "123456",
    "password",
    "qwerty123",
    "qwerty1",
    "123456789",
    "password1",
    "12345678",
    "12345",
    "abc123",
    "qwerty",
    "iloveyou",
    "Password",
    "baseball",
    "1234567",
    "111111",
    "princess",
    "football",
    "monkey",
    "sunshine"
]

Quirky body

What do I mean with ‘quirky’ body? The fact that the propert BannedPasswordList expects tab-separated values instead of comma-separated values. This will need to be taken into account in the PowerShell script.

PowerShell script

I will break down the PowerShell script in the following steps:

  1. Create a function to execute API operations on the ‘settings’ endpoint using an access token
  2. Combine the banned password list with the parameter file
  3. Update the settings in Entra ID

Let’s initialize the script:

1
2
3
4
5
6
7
8
9
[CmdLetBinding()]
Param (
    [Parameter(Mandatory,
        HelpMessage = "Enter the path of the parameter folder of authentication methods setting.")]
    [String]$ParameterFolderPath,
    [Parameter(Mandatory,
        HelpMessage = "Enter the file path of the banned password list.")]
    [String]$TenantBannedPasswordsFilePath
)

Now we declare the function to update the EntraID settings via the beta endpoint:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
function Set-EntraIdSetting {
    param (
        [Parameter(Mandatory,
            HelpMessage = "Provide the name of the settings to create/update.")]
        [Object]$TargetSettingName,
        [Parameter(Mandatory,
            HelpMessage = "Provide the file path of the settings to create/update.")]
        [Object]$SettingFilePath
    )
    # Get the access token for the Microsoft Graph API
    $settingsUri = "https://graph.microsoft.com/beta/settings"

    Write-Output "##[command]Get access token for the Microsoft Graph API"
    $accessToken = (Get-AzAccessToken -ResourceTypeName MSGraph -AsSecureString).Token
    # set the params needed for the REST API requests
    $params = @{
        Method         = 'Get'
        Uri            = $settingsUri
        Authentication = 'Bearer'
        Token          = $accessToken
        ContentType    = 'application/json'
    }
    # Wrap the request in a try catch to ensure stopping errors
    try {
        $request = (Invoke-RestMethod @params).value
    }
    catch {
        Throw $_
    }
    # Check if the request varialbe has a value
    if ($request) {
        Write-Output "##[command]Found settings. Checking for setting '$TargetSettingName'"
        $targetSettingObject = $request | Where-Object { $_.displayName -eq $TargetSettingName }
    }
    # Continue checking if we have targeted the correct settings, and update the params accordingly
    if ($targetSettingObject) {
        Write-Output "##[command]Found existing $TargetSettingName. Updating setting according to provided config."
        $passwordSettingsUri = $settingsUri + '/' + $targetSettingObject.id
        $params.Uri = $passwordSettingsUri
        $params.Method = 'Patch'
        $body = Get-Content -Path $SettingFilePath | ConvertFrom-Json -Depth 10
        $body.PSObject.properties.remove('templateId')
        $jsonBody = $body | ConvertTo-Json -Depth 10
        try {
            $settingRequest = Invoke-RestMethod @params -Body $jsonBody
        }
        catch {
            throw $_
        }
    }
    # Check if the setting does not exist. If this is the case we just post the entire template.
    elseif (!$targetSettingObject) {
        Write-Output "##[command]No existing '$TargetSettingName'. Creating new '$TargetSettingName' according to provided config."
        $jsonBody = Get-Content -Path $SettingFilePath
        $params.Method = 'Post'

        try {
            $settingRequest = Invoke-RestMethod @params -Body $jsonBody
        }
        catch {
            throw $_
        }
    }
    return $settingRequest
}

Now we update the banned password list values of the parameter file:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
Write-Output "##[command]Updating banned password list"
$bannedPasswords = Get-Content -Path $TenantBannedPasswordsFilePath | ConvertFrom-Json
$bannedPasswordsList = $null
$tab = [char]9

Write-Output "##[command]Looping the banned password list and adding tabs needed for the REST API call."
foreach ($bannedPassword in $bannedPasswords) {
    $bannedPasswordsList += $bannedPassword + $tab
}

Write-Output "##[command]Trimming the banned password list to exclude the last tab."
$trimmedPasswordList = $bannedPasswordsList -replace ".{1}$"
$bannedPasswordsSetting = Get-Content -Path "$ParameterFolderPath\passwordSettings.json" | ConvertFrom-Json -Depth 5 -AsHashtable
    ($bannedPasswordsSetting.values | Where-Object { $_.name -eq 'BannedPasswordList' }).value = $trimmedPasswordList
$bannedPasswordsSetting | ConvertTo-Json -Depth 5 | Out-File "$ParameterFolderPath\updatedPasswordSettings.json"

After this we have successfully created a new JSON-file containing the entire configuration (parameters). This file will be used as body in the next step, where we present it to our previously written function:

1
2
3
4
5
6
7
try {
    Set-EntraIdSetting -TargetSettingName 'Password Rule Settings' -SettingFilePath "$ParameterFolderPath\updatedPasswordSettings.json"
    Write-Output "Settings updated successfully!"
}
catch {
    throw
}

Azure DevOps YAML Pipeline

The pipeline is where the magic sauce comes into play. Make sure you have service connections setup to the various tenants you wish to manage. For each tenant you can add a separate stage, as per the example below. Because this is a small configuration change, the pipeline is not that complicated to set up. First let’s add the correct trigger:

1
2
3
4
5
6
7
trigger:
  branches:
    include:
    - main
  paths:
    include:
    - bannedPasswords/parameters

Triggering on the branch and path makes sure the CI/CD runs as expected. You can add more checks before the first deployment stage, or better: add a test tenant stage to validate your configuration before pushing to production. Next we configure the pool, variables and stages of the pipeline. That’s all it needs to deploy on every commit to main in the designated feature parameter folder.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
pool:
  vmImage: ubuntu-latest

variables:
  - name: ParameterFolderPath
    value: bannedPasswords/parameters

stages:
  - stage: TenantA
    jobs:
    - job: TenantA
      displayName: Updating Password Settings in Tenant A
      steps:
        - task: AzurePowerShell@5
          displayName: Setting the configuration
          inputs:
            azureSubscription: "TenantA-AuthenticationMethods-SPN"
            ScriptType: "FilePath"
            ScriptPath: "$(System.DefaultWorkingDirectory)/bannedPasswords/code/Set-PasswordSettings.ps1"
            ScriptArguments:
              -ParameterFolderPath "$(System.DefaultWorkingDirectory)/$(ParameterFolderPath)"
              -TenantBannedPasswordsFilePath "$(System.DefaultWorkingDirectory)/$(ParameterFolderPath)/bannedPasswords-TenantA.json"
            azurePowerShellVersion: LatestVersion

Adding another tenant is as easy as copy-pasting the previous stage and changing the parameters:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
  - stage: TenantB
    jobs:
    - job: TenantB
      displayName: Updating Password Settings in Tenant B
      steps:
        - task: AzurePowerShell@5
          displayName: Setting the configuration
          inputs:
            azureSubscription: "TenantB-AuthenticationMethods-SPN"
            ScriptType: "FilePath"
            ScriptPath: "$(System.DefaultWorkingDirectory)/bannedPasswords/code/Set-PasswordSettings.ps1"
            ScriptArguments:
              -ParameterFolderPath "$(System.DefaultWorkingDirectory)/$(ParameterFolderPath)"
              -TenantBannedPasswordsFilePath "$(System.DefaultWorkingDirectory)/$(ParameterFolderPath)/bannedPasswords-TenantB.json"
            azurePowerShellVersion: LatestVersion

Running the pipeline

Let’s see what my pipeline does when I run it…

pipeline

Success!

pipeline-details

Potential Pitfalls and Best Practices

This setup makes you versatile in configuring banned passwords for your environment(s). As always, stay aware of the pitfalls:

  • Ensure password policies align with specific tenant compliance requirements
  • Always implement a 4-eyes principle approval workflow in your automation
  • Test thoroughly in staged environments
  • Regularly review banned password lists and update accordingly
  • Implement comprehensive logging
  • Use the principle of least privilege gor your automation accounts

Conclusion

That concludes this blog about configuring banned password lists via the Graph API with PowerShell. We have successfully:

✅ Created a folder structure for our files
✅ Wrote an intermediate PowerShell script, including a function, to configure the Entra ID settings.
✅ Added a YAML-pipeline to automatically deploy the code to Entra ID

Hopefully this provides you with a jump start into managing Entra ID(s) in an automated fashion. You can easily expand on this automation by adding new configuration of Entra ID like Conditional Access Policies or even PIM Eligible Role Assignments! As always, leave a comment in LinkedIN if you have any more questions. Happy coding! ☕