Using Playwright to test Azure Active Directory secured APIs
Tags: playwright, azure, testing
Published: 2022-11-28 11:57:01

So you've built yourself a great API, and because you're smart you've secured it using JSON Web Tokens that you need to get from Azure Active Directory. Nice. As it's a user-focussed API you've implemented an authorisation model based on the user scopes defined in the access token. Smart.

However, now you want to perform end-to-end testing of your API endpoints, but you've suddenly realised that you need a user to authenticate themselves with Azure Active Directory to generate the access token you need.

If you were using the [Client Credentials OAuth2 grant type](https://learn.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-client-creds-grant-flow) you wouldn't have this problem, but that would only allow you to use application permissions and not user delegated scopes as you need. So using the [Authorization Code OAuth2 grant type](https://learn.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-auth-code-flow) you're now presented with the challenge of automating that authentication process.

One way of doing this would be to use the [Resource Owner Password Credentials (ROPC) OAuth2 grant type] (https://learn.microsoft.com/en-us/azure/active-directory/develop/v2-oauth-ropc), which allows you to simply pass the username and password with the token request and be given back an access token. But this grant type is considered insecure and should be avoided if possible. It is also proposed to be omitted from the [OAuth2.1 standard](https://oauth.net/2.1/).

A better way to achieve this would be to use the open source browser automation framework [Playwright](https://playwright.dev/), which offers the ability to test applications in Chromium, Firefox and WebKit with a single API, using .NET, Python, Java, or Node.js.

While Playwright is a fully-fledged automation framework for testing browser-based applications, in our scenario we're only interested in automating the Azure Active Directory authentication process. This will enable us to obtain an access token with user-scoped claims to allow us to test our API authorisation model.

This example uses a .NET 6.0 console application (a Node version can be found in the comments) with Playwright and the [Microsoft Authentication Library](https://learn.microsoft.com/en-us/azure/active-directory/develop/msal-authentication-flows) (MSAL). The token acquisition process uses the Authorization Code grant type which following successful authentication of a user will return an authorization code to the specified redirect URI. This is the part that will be automated using Playwright. The returned authorization code is then exchanged for an access token using MSAL.

![Authorization Code Grant Type](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/esgdva01ixmto9a95rbi.png)

A client [Application Registration](https://learn.microsoft.com/en-us/azure/active-directory/develop/quickstart-register-app) is required to be registered in your Azure Active Directory tenant, with all required user permissions consented, and a redirect URI configured to receive the authorization code. This example uses [https://oidcdebugger.com/](https://oidcdebugger.com/) which is then accessed by Playwright to retrieve the code. This could however just as easily be your own service.

Your tenant id, client id, and scope are required for the sample to function. In the snippet below they are shown as placeholders. In the GitHub repository these values are set from either an `appsettings.json` or `usersecrets.json` file. This is only designed as an example. If you are running this anywhere other than locally it's recommended to store these values in [Azure Key Vault secrets](https://learn.microsoft.com/en-us/azure/key-vault/secrets/about-secrets) and access them using [Managed Identity](https://learn.microsoft.com/en-us/azure/active-directory/managed-identities-azure-resources/overview).

``` cs
// Get client app related related settings
string tenant_id = "< AAD TENANT ID >";
string client_id = "< AAD CLIENT ID >";
string scope = "< API SCOPE >";

// The redirect uri being used here could be any service that you can use to access the auth code
// after it has been redirected from Azure Active Directory.
// This is using https://oidcdebugger.com which of course determines how you extract the auth code
// from page at the step lower down to use to exchange for an access token
string redirect_uri = "https://oidcdebugger.com/debug";

// Define authority and login uri
string authority = $"https://login.microsoftonline.com/{tenant_id}";
string login_uri = $"{authority}/oauth2/v2.0/authorize?client_id={client_id}&redirect_uri={redirect_uri}&scope={scope}&response_type=code&prompt=login";

// Create a Playwright instance
Console.WriteLine("Creating Playwright instance");
using var playwright = await Playwright.CreateAsync();

// Launch an instance of Chrome
Console.WriteLine("Launching Chrome");
await using var browser = await playwright.Chromium.LaunchAsync(new BrowserTypeLaunchOptions()
{
    Headless = true
});

// Create a browser page
var page = await browser.NewPageAsync();

// Navigate to the login screen
Console.WriteLine($"Navigating to {login_uri}");
await page.GotoAsync(login_uri);

// Enter username
Console.WriteLine("Entering username");
await page.FillAsync("input[name='loginfmt']", "< USERNAME >");
await page.ClickAsync("input[type=submit]");

// Wait until page has changed and is loaded
await page.WaitForLoadStateAsync(LoadState.NetworkIdle);

// Enter password
Console.WriteLine("Entering password");
await page.FillAsync("input[name='passwd']", "< PASSWORD >");
await page.ClickAsync("input[type=submit]");

// Wait until page has changed and is loaded
await page.WaitForLoadStateAsync(LoadState.NetworkIdle);

// Extract the auth code from the page we've redirected it to
Console.WriteLine("Extract auth code");
var authCode = await page.InnerTextAsync("#debug-view-component > div.debug__callback-header > div:nth-child(4) > p");

// Close the browser
await browser.CloseAsync();

// Build an MSAL confidential client
var app = ConfidentialClientApplicationBuilder.Create(client_id)
    .WithAuthority(authority)
    .WithRedirectUri(redirect_uri)
    .WithClientSecret("< CLIENT SECRET >")
    .Build();

// Get access token with code exchange
Console.WriteLine("Request access token with MSAL");
AuthenticationResult result = await app.AcquireTokenByAuthorizationCode(new string[] { scope }, authCode)
    .ExecuteAsync();

// Display the access token from the response
Console.WriteLine("Access token retrieved:\n");
Console.WriteLine(result.AccessToken);

```
Once you have exchanged your authorization code for an access token, you are free to use that token to call your API endpoints passing the token in the `Authorization` header as usual.

It is worth noting that this example will not work if multi-factor authentication is enabled for the user to be authenticated. It is recommended to create test users in your tenant that are excluded from MFA using a [Conditional Access Policy](https://learn.microsoft.com/en-us/azure/active-directory/conditional-access/overview).

If that is not possible, then you could configure MFA to use an SMS number and use a service such as [Twilio](https://www.twilio.com/) to trigger a webhook from incoming SMS messages, which could then be included into the automated token acquisition process. This is not included in the scope of this article.

Full Code: [https://github.com/irarainey/PlaywrightTokenAcquisition](https://github.com/irarainey/PlaywrightTokenAcquisition)