Reset passwords in ASP.NET Core using delegated permissions and Microsoft Graph

This article shows how an administrator can reset passwords for local members of an Azure AD tenant using Microsoft Graph and delegated permissions. An ASP.NET Core application is used to implement the Azure AD client and the Graph client services.

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

Setup Azure App registration

The Azure App registration is setup to authenticate with a user and an application (delegated flow). An App registration “Web” setup is used. Only delegated permissions are used in this setup. This implements an OpenID Connect code flow with PKCE and a confidential client. A secret or a certificate is required for this flow.

The following delegated Graph permissions are used:

  • Directory.AccessAsUser.All
  • User.ReadWrite.All
  • UserAuthenticationMethod.ReadWrite.All

ASP.NET Core setup

The ASP.NET Core application implements the Azure AD client using the Microsoft.Identity.Web Nuget package and libraries. The following packages are used:

  • Microsoft.Identity.Web
  • Microsoft.Identity.Web.UI
    • Microsoft.Identity.Web.GraphServiceClient (SDK5)
    • or
    • Microsoft.Identity.Web.MicrosoftGraph (SDK4)

Microsoft Graph is not added directly because the Microsoft.Identity.Web.MicrosoftGraph or Microsoft.Identity.Web.GraphServiceClient adds this with a tested and working version. Microsoft.Identity.Web uses the Microsoft.Identity.Web.GraphServiceClient package for Graph SDK 5. Microsoft.Identity.Web.MicrosoftGraph uses Microsoft.Graph 4.x versions. The official Microsoft Graph documentation is already updated to SDK 5. For application permissions, Microsoft Graph SDK 5 can be used with Azure.Identity.

Search for users with Graph SDK 5 and resetting the password

The Graph SDK 5 can be used to search for users and reset the user using a delegated scope and then to reset the password using the Patch HTTP request. The Graph QueryParameters are used to find the user and the HTTP Patch is used to update the password using the PasswordProfile.

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

namespace AzureAdPasswordReset;

public class UserResetPasswordDelegatedGraphSDK5
{
    private readonly GraphServiceClient _graphServiceClient;

    public UserResetPasswordDelegatedGraphSDK5(GraphServiceClient graphServiceClient)
    {
        _graphServiceClient = graphServiceClient;
    }

    /// <summary>
    /// Directory.AccessAsUser.All User.ReadWrite.All UserAuthenticationMethod.ReadWrite.All
    /// </summary>
    public async Task<(string? Upn, string? Password)> ResetPassword(string oid)
    {
        var password = GetRandomString();

        var user = await _graphServiceClient
            .Users[oid]
            .GetAsync();

        if (user == null)
        {
            throw new ArgumentNullException(nameof(oid));
        }

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

        return (user.UserPrincipalName, password);
    }

    public async Task<UserCollectionResponse?> FindUsers(string search)
    {
        var result = await _graphServiceClient.Users.GetAsync((requestConfiguration) =>
        {
            requestConfiguration.QueryParameters.Top = 10;
            if (!string.IsNullOrEmpty(search))
            {
                requestConfiguration.QueryParameters.Search = $"\"displayName:{search}\"";
            }
            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;
    }

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

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

Search for users SDK 4

The application allows the user administration to search for members of the Azure AD tenant and finds users using a select and a filter definition. The search query parameter would probably return a better user experience.

public async Task<IGraphServiceUsersCollectionPage?> 
	FindUsers(string search)
{
	var users = await _graphServiceClient.Users.Request()
		.Filter($"startswith(displayName,'{search}') AND userType eq 'Member'")
		.Select(u => new
		{
			u.Id,
			u.GivenName,
			u.Surname,
			u.DisplayName,
			u.Mail,
			u.EmployeeId,
			u.EmployeeType,
			u.BusinessPhones,
			u.MobilePhone,
			u.AccountEnabled,
			u.UserPrincipalName
		})
		.GetAsync();

	return users;
}

The ASP.NET Core Razor page supports an auto complete using the OnGetAutoCompleteSuggest method. This returns the found results using the Graph request.

private readonly UserResetPasswordDelegatedGraphSDK4 _graphUsers;
public string? SearchText { get; set; }

public IndexModel(UserResetPasswordDelegatedGraphSDK4 graphUsers)
{
	_graphUsers = graphUsers;
}

public async Task<ActionResult> OnGetAutoCompleteSuggest(string term)
{
	if (term == "*") term = string.Empty;

	var usersCollectionResponse = await _graphUsers.FindUsers(term);

	var users = usersCollectionResponse!.ToList();

	var usersDisplay = users.Select(user => new
	{
		user.Id,
		user.UserPrincipalName,
		user.DisplayName
	});

	SearchText = term;

	return new JsonResult(usersDisplay);
}

The Razor Page can be implemented using Bootstrap or whatever CSS framework you prefer.

Reset the password for user X using Graph SDK 4

The Graph service supports reset a password using a delegated permission. The user is requested using the OID and a new PasswordProfile is created updating the password and forcing a one time usage.

/// <summary>
/// Directory.AccessAsUser.All 
/// User.ReadWrite.All 
/// UserAuthenticationMethod.ReadWrite.All
/// </summary>
public async Task<(string? Upn, string? Password)> 
	ResetPassword(string oid)
{
	var password = GetRandomString();

	var user = await _graphServiceClient.Users[oid]
		.Request().GetAsync();

	if (user == null)
	{
		throw new ArgumentNullException(nameof(oid));
	}

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

	return (user.UserPrincipalName, password);
}

The Razor Page sends a post request and resets the password using the user principal name.

public async Task<IActionResult> OnPostAsync()
{
	var id = Request.Form
		.FirstOrDefault(u => u.Key == "userId")
		.Value.FirstOrDefault();
	var upn = Request.Form
		.FirstOrDefault(u => u.Key == "userPrincipalName")
		.Value.FirstOrDefault();

	if(!string.IsNullOrEmpty(id))
	{
		var result = await _graphUsers.ResetPassword(id);
		Upn = result.Upn;
		Password = result.Password;
		return Page();
	}

	return Page();
}

Running the application

When the application is started, a user password can be reset and updated. It is important to block this function for non-authorized users as it is possible to reset any account without further protection. You could PIM this application using an azure AD security group or something like this.

Notes

Using Graph SDK 4 is hard as almost no docs now exist, Graph has moved to version 5. Microsoft Graph SDK 5 has many breaking changes and is supported by Microsoft.Identity.Web using the Microsoft.Identity.Web.GraphServiceClient package.

High user permissions are used in this and it is important to protection this API or the users that can use the application.

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

https://github.com/AzureAD/microsoft-identity-web/blob/jmprieur/Graph5/src/Microsoft.Identity.Web.GraphServiceClient/Readme.md

Leave a comment

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