From b9f626b37f267f4acf9811987d932d79d2e6f5d3 Mon Sep 17 00:00:00 2001
From: Tom Weihe <65891188+ToWe0815@users.noreply.github.com>
Date: Mon, 28 Oct 2024 10:31:57 +0100
Subject: [PATCH] feat(fake-auth): add callback event to fake auth handler
(#800)
---
.../Context/LocalFakeZitadelAuthContext.cs | 91 +++++++
.../Events/LocalFakeZitadelEvents.cs | 12 +
.../Handler/LocalFakeZitadelHandler.cs | 107 +++++----
.../Options/LocalFakeZitadelOptions.cs | 115 ++++-----
.../ZitadelFakeAuthenticationHandler.Test.cs | 116 +++++----
.../FakeAuthenticationHandlerWebFactory.cs | 223 +++++++++---------
6 files changed, 403 insertions(+), 261 deletions(-)
create mode 100644 src/Zitadel/Authentication/Events/Context/LocalFakeZitadelAuthContext.cs
create mode 100644 src/Zitadel/Authentication/Events/LocalFakeZitadelEvents.cs
diff --git a/src/Zitadel/Authentication/Events/Context/LocalFakeZitadelAuthContext.cs b/src/Zitadel/Authentication/Events/Context/LocalFakeZitadelAuthContext.cs
new file mode 100644
index 00000000..d67c01f3
--- /dev/null
+++ b/src/Zitadel/Authentication/Events/Context/LocalFakeZitadelAuthContext.cs
@@ -0,0 +1,91 @@
+using System.Security.Claims;
+
+namespace Zitadel.Authentication.Events.Context
+{
+ public class LocalFakeZitadelAuthContext
+ {
+ ///
+ /// Constructor.
+ ///
+ /// The created ClaimsIdentity.
+ public LocalFakeZitadelAuthContext(ClaimsIdentity identity)
+ {
+ Identity = identity;
+ }
+
+ ///
+ /// The created ClaimsIdentity.
+ ///
+ public ClaimsIdentity Identity { get; init; }
+
+ ///
+ /// The claims of the created ClaimsIdentity.
+ ///
+ public IEnumerable Claims => Identity.Claims;
+
+ ///
+ /// The "user-id" of the fake user.
+ /// Either set by the options or via HTTP header.
+ ///
+ public string FakeZitadelId => new ClaimsPrincipal(Identity).FindFirstValue("sub")!;
+
+ ///
+ /// Add a claim to the list.
+ /// This is a convenience method for modifying .
+ ///
+ /// Type of the claim (examples: ).
+ /// The value.
+ /// Type of the value (examples: ).
+ /// The issuer for this claim.
+ /// The original issuer of this claim.
+ /// The for chaining.
+ public LocalFakeZitadelAuthContext AddClaim(
+ string type,
+ string value,
+ string? valueType = null,
+ string? issuer = null,
+ string? originalIssuer = null) => AddClaim(new(type, value, valueType, issuer, originalIssuer));
+
+ ///
+ /// Add a claim to the list.
+ /// This is a convenience method for modifying .
+ ///
+ /// The claim to add.
+ /// The for chaining.
+ public LocalFakeZitadelAuthContext AddClaim(Claim claim)
+ {
+ Identity.AddClaim(claim);
+ return this;
+ }
+
+ ///
+ /// Add a single role to the identity's claims.
+ /// Note: the roles are actually "claims" but this method exists
+ /// for convenience.
+ ///
+ /// The role to add.
+ /// The for chaining.
+ public LocalFakeZitadelAuthContext AddRole(string role)
+ {
+ Identity.AddClaim(new(ClaimTypes.Role, role));
+ return this;
+ }
+
+ ///
+ /// Add multiple roles to the identity's claims.
+ /// Note: the roles are actually "claims" but this method exists
+ /// for convenience.
+ ///
+ /// The roles to add.
+ /// The for chaining.
+ public LocalFakeZitadelAuthContext AddRoles(string[] roles)
+ {
+ foreach (var role in roles)
+ {
+ AddRole(role);
+ }
+
+ return this;
+ }
+ }
+}
diff --git a/src/Zitadel/Authentication/Events/LocalFakeZitadelEvents.cs b/src/Zitadel/Authentication/Events/LocalFakeZitadelEvents.cs
new file mode 100644
index 00000000..2aaed257
--- /dev/null
+++ b/src/Zitadel/Authentication/Events/LocalFakeZitadelEvents.cs
@@ -0,0 +1,12 @@
+using Zitadel.Authentication.Events.Context;
+using Zitadel.Authentication.Handler;
+
+namespace Zitadel.Authentication.Events;
+
+public class LocalFakeZitadelEvents
+{
+ ///
+ /// Invoked after a ClaimsIdentity has been generated in the .
+ ///
+ public Func OnZitadelFakeAuth { get; set; } = context => Task.CompletedTask;
+}
diff --git a/src/Zitadel/Authentication/Handler/LocalFakeZitadelHandler.cs b/src/Zitadel/Authentication/Handler/LocalFakeZitadelHandler.cs
index 09eaf3d0..00f4c2f3 100644
--- a/src/Zitadel/Authentication/Handler/LocalFakeZitadelHandler.cs
+++ b/src/Zitadel/Authentication/Handler/LocalFakeZitadelHandler.cs
@@ -1,52 +1,55 @@
-using System.Security.Claims;
-using System.Text.Encodings.Web;
-
-using Microsoft.AspNetCore.Authentication;
-using Microsoft.Extensions.Logging;
-using Microsoft.Extensions.Options;
-
-using Zitadel.Authentication.Options;
-
-namespace Zitadel.Authentication.Handler;
-
-#if NET8_0_OR_GREATER
-internal class LocalFakeZitadelHandler(
- IOptionsMonitor options,
- ILoggerFactory logger,
- UrlEncoder encoder)
- : AuthenticationHandler(options, logger, encoder)
-#else
-internal class LocalFakeZitadelHandler(
- IOptionsMonitor options,
- ILoggerFactory logger,
- UrlEncoder encoder,
- ISystemClock clock)
- : AuthenticationHandler(options, logger, encoder, clock)
-#endif
-{
- private const string FakeAuthHeader = "x-zitadel-fake-auth";
- private const string FakeUserIdHeader = "x-zitadel-fake-user-id";
-
- protected override Task HandleAuthenticateAsync()
- {
- if (Context.Request.Headers.TryGetValue(FakeAuthHeader, out var value) && value == "false")
- {
- return Task.FromResult(AuthenticateResult.Fail($"""The {FakeAuthHeader} was set with value "false"."""));
- }
-
- var hasId = Context.Request.Headers.TryGetValue(FakeUserIdHeader, out var forceUserId);
-
- var claims = new List
- {
- new(ClaimTypes.NameIdentifier, hasId ? forceUserId.ToString() : Options.FakeZitadelOptions.FakeZitadelId),
- new("sub", hasId ? forceUserId.ToString() : Options.FakeZitadelOptions.FakeZitadelId),
- }.Concat(Options.FakeZitadelOptions.AdditionalClaims)
- .Concat(Options.FakeZitadelOptions.Roles.Select(r => new Claim(ClaimTypes.Role, r)));
-
- var identity = new ClaimsIdentity(claims, ZitadelDefaults.FakeAuthenticationScheme);
-
- return Task.FromResult(
- AuthenticateResult.Success(
- new(new(identity), ZitadelDefaults.FakeAuthenticationScheme)));
- }
-}
+using System.Security.Claims;
+using System.Text.Encodings.Web;
+
+using Microsoft.AspNetCore.Authentication;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
+
+using Zitadel.Authentication.Options;
+
+namespace Zitadel.Authentication.Handler;
+
+#if NET8_0_OR_GREATER
+internal class LocalFakeZitadelHandler(
+ IOptionsMonitor options,
+ ILoggerFactory logger,
+ UrlEncoder encoder)
+ : AuthenticationHandler(options, logger, encoder)
+#else
+internal class LocalFakeZitadelHandler(
+ IOptionsMonitor options,
+ ILoggerFactory logger,
+ UrlEncoder encoder,
+ ISystemClock clock)
+ : AuthenticationHandler(options, logger, encoder, clock)
+#endif
+{
+ private const string FakeAuthHeader = "x-zitadel-fake-auth";
+ private const string FakeUserIdHeader = "x-zitadel-fake-user-id";
+
+ protected override Task HandleAuthenticateAsync()
+ {
+ if (Context.Request.Headers.TryGetValue(FakeAuthHeader, out var value) && value == "false")
+ {
+ return Task.FromResult(AuthenticateResult.Fail($"""The {FakeAuthHeader} was set with value "false"."""));
+ }
+
+ var hasId = Context.Request.Headers.TryGetValue(FakeUserIdHeader, out var forceUserId);
+
+ var claims = new List
+ {
+ new(ClaimTypes.NameIdentifier, hasId ? forceUserId.ToString() : Options.FakeZitadelOptions.FakeZitadelId),
+ new("sub", hasId ? forceUserId.ToString() : Options.FakeZitadelOptions.FakeZitadelId),
+ }.Concat(Options.FakeZitadelOptions.AdditionalClaims)
+ .Concat(Options.FakeZitadelOptions.Roles.Select(r => new Claim(ClaimTypes.Role, r)));
+
+ var identity = new ClaimsIdentity(claims, ZitadelDefaults.FakeAuthenticationScheme);
+
+ // Callback to enable users to manipulate the ClaimsIdentity before it is used.
+ Options.FakeZitadelOptions.Events.OnZitadelFakeAuth.Invoke(new(identity));
+
+ return Task.FromResult(
+ AuthenticateResult.Success(
+ new(new(identity), ZitadelDefaults.FakeAuthenticationScheme)));
+ }
+}
diff --git a/src/Zitadel/Authentication/Options/LocalFakeZitadelOptions.cs b/src/Zitadel/Authentication/Options/LocalFakeZitadelOptions.cs
index eb6063b4..31e488a5 100644
--- a/src/Zitadel/Authentication/Options/LocalFakeZitadelOptions.cs
+++ b/src/Zitadel/Authentication/Options/LocalFakeZitadelOptions.cs
@@ -1,54 +1,61 @@
-using System.Security.Claims;
-
-namespace Zitadel.Authentication.Options
-{
- public class LocalFakeZitadelOptions
- {
- ///
- /// The "user-id" of the fake user.
- /// This populates the "sub" and "nameidentifier" claims.
- ///
- public string FakeZitadelId { get; set; } = string.Empty;
-
- ///
- /// A list of additional claims to add to the identity.
- ///
- public IList AdditionalClaims { get; set; } = new List();
-
- ///
- /// List of roles that are attached to the identity.
- /// Note: the roles are actually "claims" but this list exists
- /// for convenience.
- ///
- public IEnumerable Roles { get; set; } = new List();
-
- ///
- /// Add a claim to the list.
- /// This is a convenience method for modifying .
- ///
- /// Type of the claim (examples: ).
- /// The value.
- /// Type of the value (examples: ).
- /// The issuer for this claim.
- /// The original issuer of this claim.
- /// The for chaining.
- public LocalFakeZitadelOptions AddClaim(
- string type,
- string value,
- string? valueType = null,
- string? issuer = null,
- string? originalIssuer = null) => AddClaim(new(type, value, valueType, issuer, originalIssuer));
-
- ///
- /// Add a claim to the list.
- /// This is a convenience method for modifying .
- ///
- /// The claim to add.
- /// The for chaining.
- public LocalFakeZitadelOptions AddClaim(Claim claim)
- {
- AdditionalClaims.Add(claim);
- return this;
- }
- }
-}
+using System.Security.Claims;
+
+using Zitadel.Authentication.Events;
+
+namespace Zitadel.Authentication.Options
+{
+ public class LocalFakeZitadelOptions
+ {
+ ///
+ /// The "user-id" of the fake user.
+ /// This populates the "sub" and "nameidentifier" claims.
+ ///
+ public string FakeZitadelId { get; set; } = string.Empty;
+
+ ///
+ /// A list of additional claims to add to the identity.
+ ///
+ public IList AdditionalClaims { get; set; } = new List();
+
+ ///
+ /// List of roles that are attached to the identity.
+ /// Note: the roles are actually "claims" but this list exists
+ /// for convenience.
+ ///
+ public IEnumerable Roles { get; set; } = new List();
+
+ ///
+ /// Gets or sets the used to enable mocking authentication data dynamically.
+ ///
+ public LocalFakeZitadelEvents Events { get; set; } = new LocalFakeZitadelEvents();
+
+ ///
+ /// Add a claim to the list.
+ /// This is a convenience method for modifying .
+ ///
+ /// Type of the claim (examples: ).
+ /// The value.
+ /// Type of the value (examples: ).
+ /// The issuer for this claim.
+ /// The original issuer of this claim.
+ /// The for chaining.
+ public LocalFakeZitadelOptions AddClaim(
+ string type,
+ string value,
+ string? valueType = null,
+ string? issuer = null,
+ string? originalIssuer = null) => AddClaim(new(type, value, valueType, issuer, originalIssuer));
+
+ ///
+ /// Add a claim to the list.
+ /// This is a convenience method for modifying .
+ ///
+ /// The claim to add.
+ /// The for chaining.
+ public LocalFakeZitadelOptions AddClaim(Claim claim)
+ {
+ AdditionalClaims.Add(claim);
+ return this;
+ }
+ }
+}
diff --git a/tests/Zitadel.Test/Authentication/ZitadelFakeAuthenticationHandler.Test.cs b/tests/Zitadel.Test/Authentication/ZitadelFakeAuthenticationHandler.Test.cs
index 957d9606..8312e26c 100644
--- a/tests/Zitadel.Test/Authentication/ZitadelFakeAuthenticationHandler.Test.cs
+++ b/tests/Zitadel.Test/Authentication/ZitadelFakeAuthenticationHandler.Test.cs
@@ -1,49 +1,67 @@
-using System.Net;
-using System.Net.Http.Json;
-using System.Security.Claims;
-
-using FluentAssertions;
-
-using Xunit;
-
-using Zitadel.Test.WebFactories;
-
-namespace Zitadel.Test.Authentication;
-
-public class ZitadelFakeAuthenticationHandler(FakeAuthenticationHandlerWebFactory factory)
- : IClassFixture
-{
- [Fact]
- public async Task Should_Be_Able_To_Call_Unauthorized_Endpoint()
- {
- var client = factory.CreateClient();
- var result =
- await client.GetFromJsonAsync("/unauthed", typeof(AuthenticationHandlerWebFactory.Unauthed)) as
- AuthenticationHandlerWebFactory.Unauthed;
- result.Should().NotBeNull();
- result?.Ping.Should().Be("Pong");
- }
-
- [Fact]
- public async Task Should_Return_Unauthorized_With_The_Fail_Header()
- {
- var client = factory.CreateClient();
- var request = new HttpRequestMessage(HttpMethod.Get, "/authed")
- {
- Headers = { { "x-zitadel-fake-auth", "false" } },
- };
- var result = await client.SendAsync(request);
- result.StatusCode.Should().Be(HttpStatusCode.Unauthorized);
- }
-
- [Fact]
- public async Task Should_Return_Authorized()
- {
- var client = factory.CreateClient();
- var result = await client.GetFromJsonAsync("/authed", typeof(AuthenticationHandlerWebFactory.Authed)) as
- AuthenticationHandlerWebFactory.Authed;
- result?.AuthType.Should().Be("ZITADEL-Fake");
- result?.UserId.Should().Be("1234");
- result?.Claims.Should().Contain(claim => claim.Key == ClaimTypes.Role && claim.Value == "User");
- }
-}
+using System.Net;
+using System.Net.Http.Json;
+using System.Security.Claims;
+
+using FluentAssertions;
+
+using Xunit;
+
+using Zitadel.Test.WebFactories;
+
+namespace Zitadel.Test.Authentication;
+
+public class ZitadelFakeAuthenticationHandler(FakeAuthenticationHandlerWebFactory factory)
+ : IClassFixture
+{
+ [Fact]
+ public async Task Should_Be_Able_To_Call_Unauthorized_Endpoint()
+ {
+ var client = factory.CreateClient();
+ var result =
+ await client.GetFromJsonAsync("/unauthed", typeof(AuthenticationHandlerWebFactory.Unauthed)) as
+ AuthenticationHandlerWebFactory.Unauthed;
+ result.Should().NotBeNull();
+ result?.Ping.Should().Be("Pong");
+ }
+
+ [Fact]
+ public async Task Should_Return_Unauthorized_With_The_Fail_Header()
+ {
+ var client = factory.CreateClient();
+ var request = new HttpRequestMessage(HttpMethod.Get, "/authed")
+ {
+ Headers = { { "x-zitadel-fake-auth", "false" } },
+ };
+ var result = await client.SendAsync(request);
+ result.StatusCode.Should().Be(HttpStatusCode.Unauthorized);
+ }
+
+ [Fact]
+ public async Task Should_Return_Authorized()
+ {
+ var client = factory.CreateClient();
+ var result = await client.GetFromJsonAsync("/authed", typeof(AuthenticationHandlerWebFactory.Authed)) as
+ AuthenticationHandlerWebFactory.Authed;
+ result?.AuthType.Should().Be("ZITADEL-Fake");
+ result?.UserId.Should().Be("1234");
+ result?.Claims.Should().Contain(claim => claim.Key == ClaimTypes.Role && claim.Value == "User");
+ }
+
+ [Fact]
+ public async Task Should_Trigger_Callback()
+ {
+ var client = factory.CreateClient();
+ var request = new HttpRequestMessage(HttpMethod.Get, "/authed")
+ {
+ Headers = { { "x-zitadel-fake-user-id", "4321" } },
+ };
+ var result = await client.SendAsync(request);
+ var content = await result.Content.ReadFromJsonAsync();
+
+ result.StatusCode.Should().Be(HttpStatusCode.OK);
+ content?.AuthType.Should().Be("ZITADEL-Fake");
+ content?.UserId.Should().Be("4321");
+ content?.Claims.Should().Contain(claim => claim.Key == "bar" && claim.Value == "foo");
+ content?.Claims.Should().Contain(claim => claim.Key == ClaimTypes.Role && claim.Value == "Admin");
+ }
+}
diff --git a/tests/Zitadel.Test/WebFactories/FakeAuthenticationHandlerWebFactory.cs b/tests/Zitadel.Test/WebFactories/FakeAuthenticationHandlerWebFactory.cs
index 39d92de3..36e74c21 100644
--- a/tests/Zitadel.Test/WebFactories/FakeAuthenticationHandlerWebFactory.cs
+++ b/tests/Zitadel.Test/WebFactories/FakeAuthenticationHandlerWebFactory.cs
@@ -1,106 +1,117 @@
-using System.Collections.Generic;
-using System.IO;
-using System.Linq;
-using System.Security.Claims;
-
-using Microsoft.AspNetCore.Builder;
-using Microsoft.AspNetCore.Hosting;
-using Microsoft.AspNetCore.Http;
-using Microsoft.AspNetCore.Mvc.Testing;
-using Microsoft.Extensions.DependencyInjection;
-using Microsoft.Extensions.Hosting;
-
-using Zitadel.Authentication;
-using Zitadel.Extensions;
-
-namespace Zitadel.Test.WebFactories;
-
-public class FakeAuthenticationHandlerWebFactory : WebApplicationFactory
-{
- #region Startup
-
- public void ConfigureServices(IServiceCollection services)
- {
- services
- .AddAuthorization()
- .AddAuthentication(ZitadelDefaults.FakeAuthenticationScheme)
- .AddZitadelFake(
- options =>
- {
- options.FakeZitadelId = "1234";
- options.AdditionalClaims = new List { new("foo", "bar"), };
- options.Roles = new List { "User" };
- });
- }
-
- public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
- {
- app.UseRouting();
-
- app.UseAuthentication();
- app.UseAuthorization();
-
- app.UseEndpoints(
- endpoints =>
- {
- endpoints.MapGet(
- "/unauthed",
- async context => { await context.Response.WriteAsJsonAsync(new Unauthed { Ping = "Pong" }); });
- endpoints.MapGet(
- "/authed",
- async context =>
- {
- await context.Response.WriteAsJsonAsync(
- new Authed
- {
- Ping = "Pong",
- AuthType = context.User.Identity?.AuthenticationType ?? string.Empty,
- UserId = context.User.FindFirstValue(ClaimTypes.NameIdentifier) ?? string.Empty,
- Claims = context.User.Claims.Select(
- c => new KeyValuePair(c.Type, c.Value))
- .ToList(),
- });
- })
- .RequireAuthorization();
- });
- }
-
- #endregion
-
- #region WebApplicationFactory
-
- protected override IHostBuilder CreateHostBuilder()
- => Host
- .CreateDefaultBuilder()
- .ConfigureWebHostDefaults(
- builder => builder
- .UseStartup());
-
- protected override IHost CreateHost(IHostBuilder builder)
- {
- builder.UseContentRoot(Directory.GetCurrentDirectory());
- return base.CreateHost(builder);
- }
-
- #endregion
-
- #region Result Classes
-
- internal record Unauthed
- {
- public string Ping { get; init; } = null!;
- }
-
- internal record Authed
- {
- public string Ping { get; init; } = null!;
-
- public string AuthType { get; init; } = null!;
-
- public string UserId { get; init; } = null!;
-
- public List> Claims { get; init; } = null!;
- }
-
- #endregion
-}
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Security.Claims;
+
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc.Testing;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
+
+using Zitadel.Authentication;
+using Zitadel.Extensions;
+
+namespace Zitadel.Test.WebFactories;
+
+public class FakeAuthenticationHandlerWebFactory : WebApplicationFactory
+{
+ #region Startup
+
+ public void ConfigureServices(IServiceCollection services)
+ {
+ services
+ .AddAuthorization()
+ .AddAuthentication(ZitadelDefaults.FakeAuthenticationScheme)
+ .AddZitadelFake(
+ options =>
+ {
+ options.FakeZitadelId = "1234";
+ options.AdditionalClaims = new List { new("foo", "bar"), };
+ options.Roles = new List { "User" };
+
+ options.Events.OnZitadelFakeAuth = context =>
+ {
+ if (context.FakeZitadelId == "4321")
+ {
+ context.AddClaim("bar", "foo");
+ context.AddRole("Admin");
+ }
+
+ return Task.CompletedTask;
+ };
+ });
+ }
+
+ public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
+ {
+ app.UseRouting();
+
+ app.UseAuthentication();
+ app.UseAuthorization();
+
+ app.UseEndpoints(
+ endpoints =>
+ {
+ endpoints.MapGet(
+ "/unauthed",
+ async context => { await context.Response.WriteAsJsonAsync(new Unauthed { Ping = "Pong" }); });
+ endpoints.MapGet(
+ "/authed",
+ async context =>
+ {
+ await context.Response.WriteAsJsonAsync(
+ new Authed
+ {
+ Ping = "Pong",
+ AuthType = context.User.Identity?.AuthenticationType ?? string.Empty,
+ UserId = context.User.FindFirstValue(ClaimTypes.NameIdentifier) ?? string.Empty,
+ Claims = context.User.Claims.Select(
+ c => new KeyValuePair(c.Type, c.Value))
+ .ToList(),
+ });
+ })
+ .RequireAuthorization();
+ });
+ }
+
+ #endregion
+
+ #region WebApplicationFactory
+
+ protected override IHostBuilder CreateHostBuilder()
+ => Host
+ .CreateDefaultBuilder()
+ .ConfigureWebHostDefaults(
+ builder => builder
+ .UseStartup());
+
+ protected override IHost CreateHost(IHostBuilder builder)
+ {
+ builder.UseContentRoot(Directory.GetCurrentDirectory());
+ return base.CreateHost(builder);
+ }
+
+ #endregion
+
+ #region Result Classes
+
+ internal record Unauthed
+ {
+ public string Ping { get; init; } = null!;
+ }
+
+ internal record Authed
+ {
+ public string Ping { get; init; } = null!;
+
+ public string AuthType { get; init; } = null!;
+
+ public string UserId { get; init; } = null!;
+
+ public List> Claims { get; init; } = null!;
+ }
+
+ #endregion
+}