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 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 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, 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.
A better way to achieve this would be to use the open source browser automation framework Playwright, 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 (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.
A client Application Registration 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/ 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 and access them using Managed Identity.
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.
If that is not possible, then you could configure MFA to use an SMS number and use a service such as Twilio 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
Top comments (10)
hi thanks the article
but i have a question
how to write this part with typescript:// Build an MSAL confidential client
var app = ConfidentialClientApplicationBuilder.Create(client_id)
.WithAuthority(authority)
.WithRedirectUri(redirect_uri)
.WithClientSecret("< CLIENT SECRET >")
.Build();
You would just need to use the msal-node package and create the client like this:
thanks a lot, but i get an error:
for redirectUri row:
ype '{ clientId: string; authority: string; redirectUri: string; clientSecret: string; }' is not assignable to type 'NodeAuthOptions'.
Object literal may only specify known properties, and 'redirectUri' does not exist in type 'NodeAuthOptions'.ts(2322)
Configuration.d.ts(80, 5): The expected type comes from property 'auth' which is declared here on type 'Configuration'
what is mean this?
Sorry, I got the confidential client mixed up with public client constructor. I've updated it now.
Here's the same example, but for Node:
thanks a lot i understand the steps, just one question
you get the code: const authCode: string = await page.innerText()
I do not understand why we have this code and where i could get it, because i add an email and password after that auth automatically
here is the sample video:
streamable.com/1p0qqe
That line is there to extract the authorization code from the website, oidcdebugger.com that we are using as a redirect. That particular line of code is referencing the HTML element that the auth code is added to after the callback, hence how we can extract it. I would suggest reading up on the OAuth2 Auth Code flow to understand what's going on here.
OAuth2 Auth Code
i see i will studying, but there is not any html object which i should use for innertext()
because i add a usermail and pwd after that ligin the webapp
I can login via this method, but when i how can i found the authCode ?
how can i send a request.post( in correct form?