Reset user account passwords using Microsoft Graph and application permissions in ASP.NET Core

This article shows how to reset a password for tenant members using a Microsoft Graph application client in ASP.NET Core. An Azure App registration is used to define the application permission for the Microsoft Graph client and the User Administrator role is assigned to the Azure Enterprise application created from the Azure App registration.

Code: https://github.com/damienbod/azuerad-reset

Create an Azure App registration with the Graph permission

An Azure App registration was created which requires a secret or a certificate. The Azure App registration has the application User.ReadWrite.All permission and is used to assign the Azure role. This client is only for application clients and not delegated clients.

Assign the User Administrator role to the App Registration

The User Administrator role is assigned to the Azure App registration (Azure Enterprise application pro tenant). You can do this by using the User Administrator Assignments and and new one can be added.

Choose the Azure App registration corresponding Enterprise application and assign the role to be always active.

Create the Microsoft Graph application client

In the ASP.NET Core application, a new Graph application can be created using the Microsoft Graph SDK and Azure Identity. The GetChainedTokenCredentials is used to authenticate using a managed identity for the production deployment or a user secret in development. You could also use a certificate. This is the managed identity from the Azure App service where the application is deployed in production.

using Azure.Identity;
using Microsoft.Graph;

namespace SelfServiceAzureAdPasswordReset;

public class GraphApplicationClientService
{
    private readonly IConfiguration _configuration;
    private readonly IHostEnvironment _environment;
    private GraphServiceClient? _graphServiceClient;

    public GraphApplicationClientService(IConfiguration configuration, IHostEnvironment environment)
    {
        _configuration = configuration;
        _environment = environment;
    }

    /// <summary>
    /// gets a singleton instance of the GraphServiceClient
    /// </summary>
    public GraphServiceClient GetGraphClientWithManagedIdentityOrDevClient()
    {
        if (_graphServiceClient != null)
            return _graphServiceClient;

        string[] scopes = new[] { "https://graph.microsoft.com/.default" };

        var chainedTokenCredential = GetChainedTokenCredentials();
        _graphServiceClient = new GraphServiceClient(chainedTokenCredential, scopes);

        return _graphServiceClient;
    }

    private ChainedTokenCredential GetChainedTokenCredentials()
    {
        if (!_environment.IsDevelopment())
        {
            // You could also use a certificate here
            return new ChainedTokenCredential(new ManagedIdentityCredential());
        }
        else // dev env
        {
            var tenantId = _configuration["AzureAdGraph:TenantId"];
            var clientId = _configuration.GetValue<string>("AzureAdGraph:ClientId");
            var clientSecret = _configuration.GetValue<string>("AzureAdGraph:ClientSecret");

            var options = new TokenCredentialOptions
            {
                AuthorityHost = AzureAuthorityHosts.AzurePublicCloud
            };

            // https://docs.microsoft.com/dotnet/api/azure.identity.clientsecretcredential
            var devClientSecretCredential = new ClientSecretCredential(
                tenantId, clientId, clientSecret, options);

            var chainedTokenCredential = new ChainedTokenCredential(devClientSecretCredential);

            return chainedTokenCredential;
        }
    }
}

Reset the password Microsoft Graph SDK 4

Once the client is authenticated, Microsoft Graph SDK can be used to implement the logic. You need to decide if SDK 4 or SDK 5 is used to implement the Graph client. Most applications must still use Graph SDK 4 but no docs exist for this anymore. Refer to Stackoverflow or try and error. The application has one method to get the user and a second one to reset the password and force a change on the next authentication. This is ok for low level security, but TAP with a strong authentication should always be used if possible.

using Microsoft.Graph;
using System.Security.Cryptography;

namespace SelfServiceAzureAdPasswordReset;

public class UserResetPasswordApplicationGraphSDK4
{
    private readonly GraphApplicationClientService _graphApplicationClientService;

    public UserResetPasswordApplicationGraphSDK4(GraphApplicationClientService graphApplicationClientService)
    {
        _graphApplicationClientService = graphApplicationClientService;
    }

    private async Task<string> GetUserIdAsync(string email)
    {
        var filter = $"startswith(userPrincipalName,'{email}')";

        var graphServiceClient = _graphApplicationClientService
            .GetGraphClientWithManagedIdentityOrDevClient();

        var users = await graphServiceClient.Users
            .Request()
            .Filter(filter)
            .GetAsync();

        return users.CurrentPage[0].Id;
    }

    public async Task<string?> ResetPassword(string email)
    {
        var graphServiceClient = _graphApplicationClientService
            .GetGraphClientWithManagedIdentityOrDevClient();

        var userId = await GetUserIdAsync(email);

        if (userId == null)
        {
            throw new ArgumentNullException(nameof(email));
        }

        var password = GetRandomString();

        await graphServiceClient.Users[userId].Request()
        .UpdateAsync(new User
        {
            PasswordProfile = new PasswordProfile
            {
                Password = password,
                ForceChangePasswordNextSignIn = true
            }
        });

        return password;
    }

    private static string GetRandomString()
    {
        var random = $"{GenerateRandom()}{GenerateRandom()}{GenerateRandom()}{GenerateRandom()}-AC";
        return random;
    }

    private static int GenerateRandom()
    {
        return RandomNumberGenerator.GetInt32(100000000, int.MaxValue);
    }
}

Reset the password Microsoft Graph SDK 5

Microsoft Graph SDK 5 can also be used to implement the logic to reset the password and force a change on the next signin.

using Microsoft.Graph;
using Microsoft.Graph.Models;
using System.Security.Cryptography;

namespace SelfServiceAzureAdPasswordReset;

public class UserResetPasswordApplicationGraphSDK5
{
    private readonly GraphApplicationClientService _graphApplicationClientService;

    public UserResetPasswordApplicationGraphSDK5(GraphApplicationClientService graphApplicationClientService)
    {
        _graphApplicationClientService = graphApplicationClientService;
    }

    private async Task<string?> GetUserIdAsync(string email)
    {
        var filter = $"startswith(userPrincipalName,'{email}')";

        var graphServiceClient = _graphApplicationClientService
            .GetGraphClientWithManagedIdentityOrDevClient();

        var result = await graphServiceClient.Users.GetAsync((requestConfiguration) =>
        {
            requestConfiguration.QueryParameters.Top = 10;
            if (!string.IsNullOrEmpty(email))
            {
                requestConfiguration.QueryParameters.Search = $"\"userPrincipalName:{email}\"";
            }
            requestConfiguration.QueryParameters.Orderby = new string[] { "displayName" };
            requestConfiguration.QueryParameters.Count = true;
            requestConfiguration.QueryParameters.Select = new string[] { "id", "displayName", "userPrincipalName", "userType" };
            requestConfiguration.QueryParameters.Filter = "userType eq 'Member'"; // onPremisesSyncEnabled eq false
            requestConfiguration.Headers.Add("ConsistencyLevel", "eventual");
        });

        return result!.Value!.FirstOrDefault()!.Id;
    }

    public async Task<string?> ResetPassword(string email)
    {
        var graphServiceClient = _graphApplicationClientService
            .GetGraphClientWithManagedIdentityOrDevClient();

        var userId = await GetUserIdAsync(email);

        if (userId == null)
        {
            throw new ArgumentNullException(nameof(email));
        }

        var password = GetRandomString();

        await graphServiceClient.Users[userId].PatchAsync(
            new User
            {
                PasswordProfile = new PasswordProfile
                {
                    Password = password,
                    ForceChangePasswordNextSignIn = true
                }
            });

        return password;
    }

    private static string GetRandomString()
    {
        var random = $"{GenerateRandom()}{GenerateRandom()}{GenerateRandom()}{GenerateRandom()}-AC";
        return random;
    }

    private static int GenerateRandom()
    {
        return RandomNumberGenerator.GetInt32(100000000, int.MaxValue);
    }
}

Any Razor page can use the service and update the password. The Razor Page requires protection to prevent any user or bot updating any other user account. Some type of secret is required to use the service or an extra id which can be created from an internal IT admin. DDOS protection and BOT protection is also required if the Razor page is deployed to a public endpoint and a delay after each request must also be implemented. Extreme caution needs to be taken when exposing this business functionality.

private readonly UserResetPasswordApplicationGraphSDK5 
	_userResetPasswordApp;

[BindProperty]
public string Upn { get; set; } = string.Empty;

[BindProperty]
public string? Password { get; set; } = string.Empty;

public IndexModel(UserResetPasswordApplicationGraphSDK5 
	userResetPasswordApplicationGraphSDK4)
{
	_userResetPasswordApp = 
		userResetPasswordApplicationGraphSDK4;
}

public void OnGet(){}

public async Task<IActionResult> OnPostAsync()
{
	if (!ModelState.IsValid)
	{
		return Page();
	}

	Password = await _userResetPasswordApp
		.ResetPassword(Upn);
	
	return Page();
}

The demo application can be started and a password from a local member can be reset.

The https://mysignins.microsoft.com/security-info url can be used to test the new password and add MFA or whatever.

Notes

You can use this solution for applications with no user. If using an administrator or a user to reset the passwords, then a delegated permission should be used with different Graph SDK methods and different Graph permissions. Extra protection is required to prevent admin accounts or break glass accounts or incorrect accounts being reset. Admin account should be no problem because these accounts should already be using FIDO2 and no password 🙂 with no recovery possible using password alone.

Links

https://aka.ms/mysecurityinfo

https://learn.microsoft.com/en-us/graph/api/overview?view=graph-rest-1.0

https://learn.microsoft.com/en-us/graph/sdks/paging?tabs=csharp

2 comments

  1. […] Reset user account passwords using Microsoft Graph and application permissions in ASP.NET Core (Damien Bowden) […]

  2. […] Reset user account passwords using Microsoft Graph and application permissions in ASP.NET Core – Damien Bowden […]

Leave a comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.