Delete all users in an Azure AD Tenant programmatically

The first part of "deleting an Azure AD tenant programmatically" is here. Today, we're going to delete all Azure AD users (except our admin account). Me an JP streamed the whole process along with the steps necessary to achieve this, but you can always skip to the code or the blog post below:

Prerequisites

  • VS Code
  • .NET Core 3.1 (or later)
  • An Azure AD B2C tenant

The Azure AD App Registration

To be able to interact with Azure AD, we need to create an App Registration. In Azure AD, got to App Registrations -> New Registration. Give it a meaningful name and select multi-tenant for supported account types. In the new App Registration, navigate to the Authentication and Add a Platform. Select Mobile & Desktop, choose the URI that works best for you (I like the MSAL one) and then make sure to select the Enable Public Client Flows option. Save this! Finally, we need to add and admin-consent to the necessary MS Graph permissions. In API Permissions press the Add a permission button, select Delegated permissions and search for the User.ReadWrite.All permission. Make sure to press the Add Permission at the bottom of the window.

Since this is a Console app, we need to provide admin consent prior to authenticating. On the API Permissions blade, press the Grant Admin consent for

We are now ready to build our user deletion code...

Let's write some code

Our code needs to

  • Authenticate the current user (interactive auth)
  • Retrieve all user objects in the directory
  • Ensure we exclude our admin account (otherwise we lose access)
  • batch up the delete operation and execute the batches.

Open up your favorite Terminal and type:
dotnet new console -n tenant_deleter

We also need to add a couple of NuGet packages to make our lives easier when interacting with Azure AD and MS Graph. In the Terminal, type:

dotnet add package Microsoft.Graph
dotnet add package Microsoft.Identity.Client

Open the project in your favorite code editor (I use VS Code) and add a new file: MsalTokenProvider.cs. Add the following code:

using System;
using System.Linq;
using System.Net.Http;
using System.Threading.Tasks;
using Microsoft.Graph;
using Microsoft.Identity.Client;

namespace tenant_deleter
{
    // todo: stop building new MsalTokenProviders for every project
    public class MsalTokenProvider : IAuthenticationProvider
    {
        public readonly IPublicClientApplication _client;
        private readonly string[] _scopes = {
             "https://graph.microsoft.com/User.ReadWrite.All"
            };

        public MsalTokenProvider(string tenantId)
        {
            _client = PublicClientApplicationBuilder
               // mt app reg in separate tenant
               // todo: move to config
               .Create("<Your Azure AD App registration Client ID>")
               .WithAuthority($"https://login.microsoftonline.com/{tenantId}")
               .Build();
        }

        public async Task AuthenticateRequestAsync(HttpRequestMessage request)
        {
            AuthenticationResult token;
            try
            {
                // get an account ------ ??????
                var account = await _client.GetAccountsAsync();
                token = await _client.AcquireTokenSilent(_scopes, account.FirstOrDefault())
                    .ExecuteAsync();
            }
            catch (MsalUiRequiredException)
            {
                token = await _client.AcquireTokenWithDeviceCode(
                    _scopes,
                    resultCallback =>
                    {
                        Console.WriteLine(resultCallback.Message);
                        return Task.CompletedTask;
                    }

                ).ExecuteAsync();
            }
            request.Headers.Authorization =
                    new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token.AccessToken);
        }
    }
}

This code is responsible for signing in to Azure AD and acquiring the Access Token for MS Graph.

Go back into Program.cs to add the necessary code:

using System;
using System.Net.Http;
using System.Threading.Tasks;
using Microsoft.Graph;

namespace tenant_deleter
{
    class Program
    {
        async static Task Main(string[] args)
        {
            string tenantId;
            if (args.Length < 1)
            {
                Console.WriteLine("gimme a tenant");
                tenantId = Console.ReadLine();
            }
            else
            {
                tenantId = args[0];
            }

            var graph = new GraphServiceClient(new MsalTokenProvider(tenantId));
            var td = new ThingDeleter(graph);
            await td.DeleteAllUsersFromTenant();
            Console.WriteLine("*fin*");
            Console.ReadLine();
        }
    }

    public class ThingDeleter
    {
        private readonly GraphServiceClient _graphServiceClient;
        public ThingDeleter(GraphServiceClient client)
        {
            _graphServiceClient = client;
        }

        // delete all users from tenant
        public async Task DeleteAllUsersFromTenant()
        {
            var me = await _graphServiceClient.Me.Request().Select(x => x.Id).GetAsync();
            var users = await _graphServiceClient.Users.Request()
                .Select(x => x.Id)
                .Top(20)
            .GetAsync();

            var batch = new BatchRequestContent();
            var currentBatchStep = 1;
            var pageIterator = PageIterator<User>
            .CreatePageIterator(
                _graphServiceClient,
                users,
                (user) =>
                {
                    if (user.Id == me.Id) return true; //don't delete me
                    var requestUrl = _graphServiceClient
                            .Users[user.Id]
                            .Request().RequestUrl;

                    var request = new HttpRequestMessage(HttpMethod.Delete, requestUrl);
                    var requestStep = new BatchRequestStep(currentBatchStep.ToString(), request, null);
                    batch.AddBatchRequestStep(requestStep);

                    if (currentBatchStep == users.Count)
                    {
                        _graphServiceClient.Batch.Request().PostAsync(batch).GetAwaiter().GetResult();
                        currentBatchStep = 1; // batches are 1-indexed, weird
                        return true;
                    }
                    currentBatchStep++;
                    return true;
                },
                (req) =>
                {
                    return req;
                }
            );
            await pageIterator.IterateAsync();
        }
    }
}

You'll notice that the Graph code uses batching to optimize the calls against MS Graph. This is necessary, especially for larger tenants with 1000s of users. The nice thing is that we can also parallelize this to make it ever more efficient :)

Now, we can build the code with dotnet build to make sure that everything builds and that we haven't missed anything.

Finally, let's run the app with dotnet run <your Tenant Id>

Job done. One task down, a few more to go! Check the main blog post on how to delete an Azure AD Tenant.

Show me the code

If you want to clone and run the app, head out to the GitHub Repo

29