Sharing Authentication in Playwright for .NET

May 02, 2024#Testing
Article
Author image.

Sarah Dutkiewicz, Senior Trainer

If you are currently trying to test pages behind authentication while using Playwright for .NET, you’re in the right place! This post stems from inner frustrations of sparse documentation that has code snippets but no real guidance on where those snippets go or how the feature event works. Can you really save an authenticated state and use it in other tests using Playwright for .NET? YES!

So… let’s see how we are doing this here at NimblePros!

Note: This blog post was written with reference to Microsoft.Playwright.NUnit version 1.43.

What is that .auth folder?

While looking at the Playwright for .NET Authentication page, I noticed that they called out to create a playwright/.auth folder and add it to your .gitignore file. I was curious to see if they would save session state to the folder automatically. However, the more I tinkered with how this might work, the more I realized that this isn’t really needed. Because… (the next question explains the “why”…)

But wait… where does my storage state get stored?

When you run your tests, it will look in the Path that you specify in context.StorageStateAsync()’s options. Keep in mind that if you follow their documentation and use a Path value of state.json, it will store the state.json in the current working directory.

For example, suppose my tests are in C:\code\SuperSecretProject\tests\SuperSecretProject.FrontEndTests. When I run my tests in Debug mode, I will find the state.json file in the C:\code\SuperSecretProject\tests\SuperSecretProject.FrontEndTests\bin\Debug\netX.0 folder of your tests project - where netX.0 matches the project’s targeted .NET version.

What kind of data is stored?

Here’s a glimpse at what’s stored in state.json:

{
    "cookies":[
        {
            "name":".AspNetCore.Antiforgery.TokenCode",
            "value":"AntiforgeryValue",
            "domain":"localhost",
            "path":"/",
            "expires":-1,
            "httpOnly":true,
            "secure":false,
            "sameSite":"Strict"
        },
        // A bunch of marketing cookies        
        {
            "name":"ProjectAuth",
            "value":"AuthValue",
            "domain":"localhost",
            "path":"/",
            "expires":-1,
            "httpOnly":true,
            "secure":true,"
            sameSite":"Lax"
        }
    ],    
    "origins":[]
    }

All these code snippets - where do they go?

I’m glad you asked, because they don’t really get into this in the documentation. That’s why I’m blogging this.

Cookies and local storage values can be stored in a file through the help of the BrowserContext.StorageStateAsync() method.

The documentation shows snippets for saving state and creating a new context with saved state. But they are snippets of syntax - not snippets with context.

Before we get into the snippets, let’s lay out some of our classes and how they relate.

Sets storage state if not authenticated
Uses AuthenticatedPage for navigation
LoginPage
LoginAsync()
BasePageObject
AuthenticatedPageBaseTest
+ IPage AuthenticatedPage
- AuthenticatedPlaywrightDriver driver
+OneTimeTearDown()
+Setup()
AuthenticatedPlaywrightDriver
- IPlaywright Playwright
- IBrowser browser
- IBrowserContext context
~Task<<IPage>> CreateAuthenticatedPage()
-Task<<IBrowserContext>> CreateAuthenticatedContext()
-Task<<string>> LoadStorageStateFromFile()
-Dispose()
PageTest
PageBehindAuthenticationPageTest
+TestSomething1()
+TestSomething2()

Or for those who aren’t familiar with reading UML:

  • LoginPage inherits from BasePageObject. BasePageObject is a class we use for all page objects that contains the Base URI for testing.
  • AuthenticatedPlaywrightDriver depends on LoginPage. This is an intentional tight coupling, as we need an IPage to navigate and get our authenication details. LoginPage fits the bill.
  • AuthenticatedPlaywrightDriver has many private functions. The CreateAuthenticatedPage() is marked as internal and can be used by the assembly.
  • The AuthenticatedPageBaseTest is the base class for tests for pages behind authentication. It inherits from the PageTest class provided by Playwright. The AuthenticatedPageBaseTest has a property that is a AuthenticatedPlaywrightDriver.
  • PageBehindAuthenticationPageTest inherits from AuthenticatedPageBaseTest.

So imagine you have a SuperSecretProject with a page to view related resources that only Admins can see. You want to write an automated test to ensure it works correctly:

  • You need a way to either load the session state from file or create the file if it doesn’t exist.
  • You need a login page to go through the steps of logging into the site and save the state to the state file.
  • You want to navigate using a page with a browser context that has the saved session state.
  • You need a page object to represent your SuperSecretAdminPage. This is the equivalent of PageBehindAuthenticationPageTest above.

How do we create a new browser context?

Their snippets of code looks like this:

Save Storage State

// Save storage state into the file.
await context.StorageStateAsync(new()
{
    Path = "state.json"
});

Create Context with Storage State

// Create a new context with the saved storage state.
var context = await browser.NewContextAsync(new()
{
    StorageStatePath = "state.json"
});

But where do these snippets go? Playwright uses browser contexts to run the tests. So this is the flow I’ve gone with:

Create Browser Context
Create Context
Auth and Save
Yes
No
Load contents from file
Create new file if not exists
Is storage state null or empty?
Return authenticated page using authenticated context
Do login steps with LoginPage page object
Save Storage State Snippet Goes Here
Create Context with Storage State Snippet Goes Here
Launch Playwright via CreateAsync method
Launch a browser
Create a browser context

From what I’ve seen in the documentation, it doesn’t appear to be something that can be set on the default browser. So we are creating our context in AuthenticatedPlaywrightDriver’s CreateAuthenticatedContext():

private async Task<IBrowserContext> CreateAuthenticatedContext()
{
  // Load storage state from file
  var storageState = await LoadStorageStateFromFile("state.json");

  // Create a new browser context with the loaded storage state
  var contextOptions = new BrowserNewContextOptions
  {
    StorageState = storageState.ToString()
  };
  _context = await _browser!.NewContextAsync(contextOptions);
  if (string.IsNullOrEmpty(storageState))
  {
    // If the state file didn't exist, a new file is created
    // That would get you here to this point
    // Since the login page object handles logging into the portal,
    // We will add that dependency here.
    var loginPage = new LoginPage(await _context.NewPageAsync());
    await loginPage.LoginAsync();
  }
  return _context;
}

Our LoadStorageStateFromFile() method isn’t anything special:

private async Task<string> LoadStorageStateFromFile(string filePath)
{
    if (!File.Exists(filePath))
    {
        // Create the file and release the lock
        File.Create(filePath).Dispose();
    }

    // Read the storage state file
    return await File.ReadAllTextAsync(filePath);
}

Where do we save the browser context?

As for LoginAsync(), it navigates the login page, enters in credentials, and then calls the Save snippet to save the value to the state.json file. This could be entering credentials in textboxes, clicking links for SSO, or whatever steps you need to take for authentication. Once the authentication is done, then you call the snippet of code to save the state.

How do we use this with Multiple Pages?

All pages behind authentication can follow the pattern that PageBehindAuthenticationPageTest does:

  • Inherit from AuthenticatedPageBaseTest.
  • Use the AuthenticatedPage as the IPage object for your tests. This is what contains the storage state.

What’s in the AuthenticatedPageBaseTest?

The authenticated page base test file looks like this:

using Microsoft.Playwright;

namespace NimblePros.SuperSecretProject.FrontEndTests.Tests.Auth;

internal class AuthenticatedPageBaseTest : PageTest
{
  private AuthenticatedPlaywrightDriver driver = new AuthenticatedPlaywrightDriver();

  public IPage AuthenticatedPage;
  
  [SetUp]
  public async Task Setup()
  {
    AuthenticatedPage = await driver.CreateAuthenticatedPage();
  }

  [OneTimeTearDown]
  public void OneTimeTearDown()
  {
    driver.Dispose();
  }
}

What’s the AuthenticatedPlaywrightDriver?

This is how we launch our Playwright, launch the browser, create the context, and create a new page with the authentication. You can find the code here in this gist for AuthenticatedPlaywrightDriver.cs.

What if my Authentication Token is in Session Storage?

This post focuses on local storage and cookies. However, we also have a post on Sharing Session Storage in Playwright for .NET. That post will walk you through their snippets for accessing session storage.

Conclusion

I hope this blog post has helped enlighten you on how to save state and reuse it in Playwright for .NET.

If you are getting started with Playwright for .NET, we have a couple on-demand webinars that may help you as well:

If you are interested in having us train your devs on testing or working with Playwright for .NET, contact us today!


Copyright © 2024 NimblePros - All Rights Reserved