-
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add C3D.Extensions.Playwright.AspNetCore.Authentication and tests
- Loading branch information
1 parent
cdbb86e
commit 1f5ed7f
Showing
15 changed files
with
556 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
35 changes: 35 additions & 0 deletions
35
...ight/AspNetCore.Authentication/C3D.Extensions.Playwright.AspNetCore.Authentication.csproj
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
<Project Sdk="Microsoft.NET.Sdk"> | ||
|
||
<PropertyGroup> | ||
<TargetFrameworks>net6.0;net7.0</TargetFrameworks> | ||
<ImplicitUsings>enable</ImplicitUsings> | ||
<Nullable>enable</Nullable> | ||
<AssemblyTitle>$(AssemblyTitle) Authentication</AssemblyTitle> | ||
</PropertyGroup> | ||
|
||
<ItemGroup> | ||
<None Include="version.json" /> | ||
</ItemGroup> | ||
|
||
<ItemGroup> | ||
<None Include="README.md" /> | ||
</ItemGroup> | ||
|
||
<ItemGroup> | ||
<PackageTag Include="Authentication" /> | ||
<PackageTag Include="Basic" /> | ||
</ItemGroup> | ||
|
||
<ItemGroup> | ||
<None Include="README.md" /> | ||
<None Include="version.json" /> | ||
</ItemGroup> | ||
|
||
<ItemGroup> | ||
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="6.0.21" Condition="'$(TargetFramework)'=='net6.0'" /> | ||
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="7.0.10" Condition="'$(TargetFramework)'=='net7.0'" /> | ||
<PackageReference Include="idunno.Authentication.Basic" Version="2.3.1" /> | ||
<ProjectReference Include="..\AspNetCore\C3D.Extensions.Playwright.AspNetCore.csproj" /> | ||
</ItemGroup> | ||
|
||
</Project> |
12 changes: 12 additions & 0 deletions
12
src/C3D/Extensions/Playwright/AspNetCore.Authentication/CredentialValidationExtensions.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
using System.Security.Claims; | ||
|
||
namespace Microsoft.AspNetCore.Authentication; | ||
|
||
public static class CredentialValidationExtensions { | ||
public static Claim DefaultRoleClaim<TOptions>(this ResultContext<TOptions> context, string roleName) | ||
where TOptions : AuthenticationSchemeOptions | ||
=> new(ClaimTypes.Role, | ||
roleName, | ||
ClaimValueTypes.String, | ||
context.Options.ClaimsIssuer); | ||
} |
19 changes: 19 additions & 0 deletions
19
src/C3D/Extensions/Playwright/AspNetCore.Authentication/Handlers/BasicAuthHandler.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
namespace C3D.Extensions.Playwright.AspNetCore.Authentication.Handlers; | ||
|
||
public class BasicAuthHandler : DelegatingHandler | ||
{ | ||
private readonly string? username; | ||
private readonly string? password; | ||
|
||
public BasicAuthHandler(string? username, string? password) | ||
{ | ||
this.username = username; | ||
this.password = password; | ||
} | ||
|
||
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) | ||
{ | ||
request.Headers.Authorization = BasicAuthHeaderUtilities.BasicAuthHeader(username, password); | ||
return base.SendAsync(request, cancellationToken); | ||
} | ||
} |
24 changes: 24 additions & 0 deletions
24
...tensions/Playwright/AspNetCore.Authentication/HostBuilderBasicAuthenticationExtensions.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
using idunno.Authentication.Basic; | ||
using Microsoft.Extensions.DependencyInjection; | ||
using System.Security.Claims; | ||
|
||
namespace Microsoft.Extensions.Hosting; | ||
|
||
public static class HostBuilderBasicAuthenticationExtensions | ||
{ | ||
/// <summary> | ||
/// Registers a basic authentication scheme that succeeds for password==username and assigns the role of the username | ||
/// </summary> | ||
public static IHostBuilder AddBasicAuthentication(this IHostBuilder builder, | ||
Func<ValidateCredentialsContext, string, Task<IEnumerable<Claim>?>>? roleClaimsFunc = null) => | ||
builder.ConfigureServices(services => services.AddBasicAuthentication(roleClaimsFunc)); | ||
|
||
/// <summary> | ||
/// Uses a registered RoleManager from Microsoft.AspNetCore.Identity to lookup the role and add any role specific claims. | ||
/// </summary> | ||
/// <typeparam name="TRole">Class used for the Role</typeparam> | ||
/// <param name="services">The main service collection</param> | ||
/// <returns></returns> | ||
public static IHostBuilder AddBasicAuthentication<TRole>(this IHostBuilder builder) | ||
where TRole : class => builder.ConfigureServices(services => services.AddBasicAuthentication<TRole>()); | ||
} |
54 changes: 54 additions & 0 deletions
54
...ight/AspNetCore.Authentication/Options/WebApplicationFactoryAuthenticatedClientOptions.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,54 @@ | ||
using C3D.Extensions.Playwright.AspNetCore.Authentication.Handlers; | ||
using Microsoft.AspNetCore.Mvc.Testing; | ||
using Microsoft.AspNetCore.Mvc.Testing.Handlers; | ||
|
||
namespace C3D.Extensions.Playwright.AspNetCore.Authentication.Options; | ||
|
||
public class WebApplicationFactoryAuthenticatedClientOptions : WebApplicationFactoryClientOptions | ||
{ | ||
public WebApplicationFactoryAuthenticatedClientOptions() | ||
{ | ||
} | ||
|
||
// Copy constructor | ||
internal WebApplicationFactoryAuthenticatedClientOptions(WebApplicationFactoryClientOptions clientOptions) | ||
{ | ||
BaseAddress = clientOptions.BaseAddress; | ||
AllowAutoRedirect = clientOptions.AllowAutoRedirect; | ||
MaxAutomaticRedirections = clientOptions.MaxAutomaticRedirections; | ||
HandleCookies = clientOptions.HandleCookies; | ||
|
||
if (clientOptions is WebApplicationFactoryAuthenticatedClientOptions authOptions) | ||
{ | ||
UserName = authOptions.UserName; | ||
Password = authOptions.Password; | ||
Handlers = authOptions.Handlers; | ||
} | ||
} | ||
|
||
public string? UserName { get; set; } | ||
public string? Password { get; set; } | ||
|
||
public IEnumerable<DelegatingHandler> Handlers { get; set; } = Enumerable.Empty<DelegatingHandler>(); | ||
|
||
internal protected virtual DelegatingHandler[] CreateHandlers() | ||
{ | ||
return CreateHandlersCore().Concat(Handlers).ToArray(); | ||
|
||
IEnumerable<DelegatingHandler> CreateHandlersCore() | ||
{ | ||
if (!string.IsNullOrEmpty(UserName) || !string.IsNullOrEmpty(Password)) | ||
{ | ||
yield return new BasicAuthHandler(UserName, Password); | ||
} | ||
if (AllowAutoRedirect) | ||
{ | ||
yield return new RedirectHandler(MaxAutomaticRedirections); | ||
} | ||
if (HandleCookies) | ||
{ | ||
yield return new CookieContainerHandler(); | ||
} | ||
} | ||
} | ||
} |
92 changes: 92 additions & 0 deletions
92
src/C3D/Extensions/Playwright/AspNetCore.Authentication/README.md
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,92 @@ | ||
# C3D.Extensions.Playwright.AspNetCore.Authentication | ||
|
||
An extension to `Microsoft.AspNetCore.Mvc.Testing` and `C3D.Extensions.Playwright.AspNetCore` which adds authentication support to the `WebApplicationFactory`. | ||
|
||
This allows you to write Playwright browser based tests that use and test authentication. | ||
|
||
The authentication uses the [`idunno.Authentication.Basic`](https://github.com/blowdart/idunno.Authentication) package to provide 'Basic Authentication'. | ||
This should not (normally) be used in a production environement, but provides an easy to use mechansim to generate authentication tokens on the server side, | ||
and matching credentials on the client side. | ||
|
||
## Setup | ||
|
||
When creating a 'test' host using `IHostBuilder`, you can use the `AddBasicAuthentication` extension method to enable the embedded `idunno.Authentication.Basic` authentication system. | ||
```cs | ||
builder.AddBasicAuthentication(); | ||
``` | ||
|
||
This will generate a claims user when the username == the password. | ||
The claims will include the username, displayname and role (which will all be the same). | ||
|
||
You can add an optional function to add additional claims as a parameter to the `AddBasicAuthentication` call. | ||
The function takes `ValidateCredentialsContext` and string parameters representing the context and the username/role (which are equal). | ||
It is an async function that returns `Task<IEnumerable<Claim>?>`. This allows you to not return any additional claims. | ||
|
||
There is an overload that takes the `TRole` type of the registered RoleManager from Microsoft.AspNetCore.Identity to lookup the role and add any role specific claims. | ||
This can be called as | ||
|
||
```cs | ||
builder.AddBasicAuthentication<AppRole>(); | ||
``` | ||
|
||
Obviously this is not secure in any way, and should only be used in a test scenario, e.g. during Playwright testing. | ||
|
||
## Usage | ||
|
||
When you have a host that is setup to support BasicAuthention, you can then create a Playwright browser context (effectively an in-private isolated session), which will include the appropriate authentication header. | ||
There is an extension method to the `PlaywrightFixture<TProgram>` called `CreateAuthorisedPlaywrightContextPageAsync` which takes the rolename to use. | ||
This creates a new context and page (which should be disposed at the end of the test), with a Basic Authentication header with the username and password equal to the passed in role. | ||
|
||
|
||
### Sample | ||
|
||
An example of using this with `XUnit` is available in the github repository. | ||
|
||
```cs | ||
public class PlaywrightAuthenticationFixture : PlaywrightFixture<Program> | ||
{ | ||
public PlaywrightAuthenticationFixture(IMessageSink output) : base(output) { } | ||
|
||
protected override IHost CreateHost(IHostBuilder builder) | ||
{ | ||
builder.AddBasicAuthentication(); | ||
return base.CreateHost(builder); | ||
} | ||
} | ||
``` | ||
|
||
```cs | ||
public class AuthenticationTests : IClassFixture<PlaywrightAuthenticationFixture> | ||
{ | ||
private readonly PlaywrightFixture<Program> webApplication; | ||
|
||
public AuthenticationTests(PlaywrightAuthenticationFixture webApplication, ITestOutputHelper outputHelper) | ||
{ | ||
this.webApplication = webApplication; | ||
} | ||
|
||
[Fact] | ||
public async Task RandomTest() | ||
{ | ||
await using var context = await webApplication.CreateAuthorisedPlaywrightContextPageAsync("SomeRole"); | ||
var page = context.Page; | ||
|
||
await page.GotoAsync("/Somewhere"); | ||
} | ||
} | ||
``` | ||
|
||
## HttpClient | ||
|
||
While this package is primarliy designed for use with Playwright, you may also require to use the `HttpClient` features of `Microsoft.AspNetCore.Mvc.Testing.WebApplicationFactory`. | ||
|
||
This package provides a number of overloads of the basic `CreateClient` method on `WebApplicationFactory<TProgram>`. | ||
The first takes a function to configure a `WebApplicationFactoryAuthenticatedClientOptions` which is an augmented version of `WebApplicationFactoryClientOptions` and defaults to (a copy of) the settings from `WebApplicationFactory<TProgram>.ClientOptions`. | ||
The additional properties `UserName`, `Password` and `Handlers` are available. Setting either (or both) of the authentication properties results in an `AuthenticationHeaderValue` being added to each request made. | ||
|
||
`Handlers` allows you to add additional middleware handlers into the configuration. | ||
This allows you to use the Authentication, Redirection, and Cookie handlers at the same time as custom ones without having to manually add them all. | ||
|
||
There are 2 additional overloads of `CreateClient`, one which takes `username` and `password` as parameters, and another which takes a single string `role` which is used as both `username` and `password`. | ||
These are syntactic sugar over the configuration method mentioned previously. | ||
|
89 changes: 89 additions & 0 deletions
89
...ns/Playwright/AspNetCore.Authentication/ServiceCollectionBasicAuthenticationExtensions.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,89 @@ | ||
using idunno.Authentication.Basic; | ||
using Microsoft.AspNetCore.Authentication; | ||
using Microsoft.AspNetCore.Builder; | ||
using Microsoft.AspNetCore.Identity; | ||
using Microsoft.Extensions.Logging; | ||
using System.Security.Claims; | ||
|
||
namespace Microsoft.Extensions.DependencyInjection; | ||
|
||
public static class ServiceCollectionBasicAuthenticationExtensions | ||
{ | ||
/// <summary> | ||
/// Registers a basic authentication scheme that succeeds for password==username and assigns the role of the username | ||
/// </summary> | ||
public static IServiceCollection AddBasicAuthentication(this IServiceCollection services, | ||
Func<ValidateCredentialsContext, string, Task<IEnumerable<Claim>?>>? roleClaimsFunc = null) | ||
=> services | ||
.AddAuthentication(BasicAuthenticationDefaults.AuthenticationScheme) | ||
.AddBasic(options => | ||
{ | ||
options.Realm = "Test Realm"; | ||
options.AllowInsecureProtocol = true; | ||
options.Events = new BasicAuthenticationEvents | ||
{ | ||
OnAuthenticationFailed = context => | ||
{ | ||
var loggerFactory = context.HttpContext.RequestServices.GetRequiredService<ILoggerFactory>(); | ||
var logger = loggerFactory.CreateLogger<BasicAuthenticationEvents>(); | ||
logger.LogError(context.Exception, "Authentication failed"); | ||
return Task.CompletedTask; | ||
}, | ||
OnValidateCredentials = async context => | ||
{ | ||
if (context.Username == context.Password) | ||
{ | ||
|
||
var userClaims = new[] | ||
{ | ||
// Set UserName | ||
new Claim( | ||
ClaimTypes.NameIdentifier, | ||
context.Username, | ||
ClaimValueTypes.String, | ||
context.Options.ClaimsIssuer), | ||
// Set DisplayName | ||
new Claim( | ||
ClaimTypes.Name, | ||
context.Username, | ||
ClaimValueTypes.String, | ||
context.Options.ClaimsIssuer) | ||
}; | ||
|
||
|
||
var roleClaims = roleClaimsFunc is null ? | ||
Enumerable.Repeat(context.DefaultRoleClaim(context.Username), 1) : | ||
(await roleClaimsFunc.Invoke(context, context.Username) ?? Enumerable.Empty<Claim>()); | ||
|
||
context.Principal = new ClaimsPrincipal( | ||
new ClaimsIdentity(userClaims.Concat(roleClaims), context.Scheme.Name)); | ||
context.Success(); | ||
} | ||
} | ||
}; | ||
}) | ||
.Services; | ||
|
||
/// <summary> | ||
/// Uses a registered RoleManager from Microsoft.AspNetCore.Identity to lookup the role and add any role specific claims. | ||
/// </summary> | ||
/// <typeparam name="TRole">Class used for the Role</typeparam> | ||
/// <param name="services">The main service collection</param> | ||
/// <returns></returns> | ||
public static IServiceCollection AddBasicAuthentication<TRole>(this IServiceCollection services) | ||
where TRole : class => services.AddBasicAuthentication(async (context, roleName) => | ||
{ | ||
// This bit is probably overkill for most testing needs. | ||
// Simply adding the role, regardless of whether it exists, to the claim is enough for most scenarios. | ||
// But, in case there is anything custom added to the Role under RoleManager, we lookup the role and any custom claims. | ||
var roleManager = context.HttpContext.RequestServices.GetRequiredService<RoleManager<IdentityRole>>(); | ||
var role = await roleManager.FindByNameAsync(roleName); | ||
IList<Claim> roleClaims = (role is not null ? await roleManager.GetClaimsAsync(role) : null) ?? Enumerable.Empty<Claim>().ToList(); | ||
if (role is not null) | ||
{ | ||
roleClaims.Add(context.DefaultRoleClaim(roleName)); | ||
} | ||
return roleClaims; | ||
}); | ||
|
||
} |
Oops, something went wrong.