diff --git a/.github/workflows/dotnet-test.yml b/.github/workflows/dotnet-test.yml
index 6ca6ec0b..70599487 100644
--- a/.github/workflows/dotnet-test.yml
+++ b/.github/workflows/dotnet-test.yml
@@ -13,8 +13,6 @@ jobs:
fail-fast: true
matrix:
version:
- - 6.x
- - 7.x
- 8.x
steps:
- uses: actions/checkout@v4
diff --git a/examples/Zitadel.ApiAccess/Zitadel.ApiAccess.csproj b/examples/Zitadel.ApiAccess/Zitadel.ApiAccess.csproj
index 7fb01468..dcafb70f 100644
--- a/examples/Zitadel.ApiAccess/Zitadel.ApiAccess.csproj
+++ b/examples/Zitadel.ApiAccess/Zitadel.ApiAccess.csproj
@@ -3,7 +3,7 @@
false
Exe
- net6.0
+ net8.0
enable
enable
diff --git a/examples/Zitadel.AspNet.AuthN/Pages/Authenticated.cshtml b/examples/Zitadel.AspNet.AuthN/Pages/Authenticated.cshtml
index 6ac67357..7a16838e 100644
--- a/examples/Zitadel.AspNet.AuthN/Pages/Authenticated.cshtml
+++ b/examples/Zitadel.AspNet.AuthN/Pages/Authenticated.cshtml
@@ -1,5 +1,4 @@
@page
-@using Microsoft.AspNetCore.Mvc.TagHelpers
@model Zitadel.AspNet.AuthN.Pages.Authenticated
diff --git a/examples/Zitadel.AspNet.AuthN/Pages/Authenticated.cshtml.cs b/examples/Zitadel.AspNet.AuthN/Pages/Authenticated.cshtml.cs
index 62bbde94..388cdfe2 100644
--- a/examples/Zitadel.AspNet.AuthN/Pages/Authenticated.cshtml.cs
+++ b/examples/Zitadel.AspNet.AuthN/Pages/Authenticated.cshtml.cs
@@ -3,6 +3,8 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
+using Zitadel.Authentication;
+
namespace Zitadel.AspNet.AuthN.Pages;
[Authorize]
@@ -10,7 +12,8 @@ public class Authenticated : PageModel
{
public async Task OnPostAsync()
{
- await HttpContext.SignOutAsync("Identity.External"); // Options: signs you out of ZITADEL entirely, without this you may not be reprompted for your password.
+ await HttpContext.SignOutAsync(
+ "Identity.External"); // Options: signs you out of ZITADEL entirely, without this you may not be reprompted for your password.
await HttpContext.SignOutAsync(
ZitadelDefaults.AuthenticationScheme,
new AuthenticationProperties { RedirectUri = "http://localhost:8080/loggedout" }
diff --git a/examples/Zitadel.AspNet.AuthN/Pages/LoggedOut.cshtml b/examples/Zitadel.AspNet.AuthN/Pages/LoggedOut.cshtml
index 4db4bbe4..3efd995f 100644
--- a/examples/Zitadel.AspNet.AuthN/Pages/LoggedOut.cshtml
+++ b/examples/Zitadel.AspNet.AuthN/Pages/LoggedOut.cshtml
@@ -1,5 +1,5 @@
@page
-@model LoggedOutModel
+@model Zitadel.AspNet.AuthN.Pages.LoggedOutModel
diff --git a/examples/Zitadel.AspNet.AuthN/Pages/LoggedOut.cshtml.cs b/examples/Zitadel.AspNet.AuthN/Pages/LoggedOut.cshtml.cs
index f70d8a77..eefa7c4c 100644
--- a/examples/Zitadel.AspNet.AuthN/Pages/LoggedOut.cshtml.cs
+++ b/examples/Zitadel.AspNet.AuthN/Pages/LoggedOut.cshtml.cs
@@ -1,7 +1,5 @@
-using System.Diagnostics;
-using Microsoft.AspNetCore.Mvc;
-using Microsoft.AspNetCore.Mvc.RazorPages;
-
-namespace Zitadel.AspNet.AuthN.Pages;
-
-public class LoggedOutModel : PageModel { }
+using Microsoft.AspNetCore.Mvc.RazorPages;
+
+namespace Zitadel.AspNet.AuthN.Pages;
+
+public class LoggedOutModel : PageModel;
diff --git a/examples/Zitadel.AspNet.AuthN/Program.cs b/examples/Zitadel.AspNet.AuthN/Program.cs
index d2ecb032..84bb8cf3 100644
--- a/examples/Zitadel.AspNet.AuthN/Program.cs
+++ b/examples/Zitadel.AspNet.AuthN/Program.cs
@@ -33,6 +33,6 @@
app.UseHttpsRedirection();
app.UseAuthentication();
app.UseAuthorization();
-app.UseEndpoints(endpoints => { endpoints.MapRazorPages(); });
+app.MapRazorPages();
await app.RunAsync();
diff --git a/examples/Zitadel.AspNet.AuthN/Zitadel.AspNet.AuthN.csproj b/examples/Zitadel.AspNet.AuthN/Zitadel.AspNet.AuthN.csproj
index 4bd3495e..bb5486c6 100644
--- a/examples/Zitadel.AspNet.AuthN/Zitadel.AspNet.AuthN.csproj
+++ b/examples/Zitadel.AspNet.AuthN/Zitadel.AspNet.AuthN.csproj
@@ -2,7 +2,7 @@
false
- net6.0
+ net8.0
enable
enable
diff --git a/examples/Zitadel.WebApi/Program.cs b/examples/Zitadel.WebApi/Program.cs
index ebb83d39..349fd988 100644
--- a/examples/Zitadel.WebApi/Program.cs
+++ b/examples/Zitadel.WebApi/Program.cs
@@ -41,7 +41,6 @@
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
-app.UseEndpoints(
- endpoints => { endpoints.MapControllers(); });
+app.MapControllers();
await app.RunAsync();
diff --git a/examples/Zitadel.WebApi/Zitadel.WebApi.csproj b/examples/Zitadel.WebApi/Zitadel.WebApi.csproj
index 7b1c8305..9438625e 100644
--- a/examples/Zitadel.WebApi/Zitadel.WebApi.csproj
+++ b/examples/Zitadel.WebApi/Zitadel.WebApi.csproj
@@ -2,7 +2,7 @@
false
- net6.0
+ net8.0
enable
enable
diff --git a/src/Directory.Build.props b/src/Directory.Build.props
index b9611d57..378d2ca5 100644
--- a/src/Directory.Build.props
+++ b/src/Directory.Build.props
@@ -1,11 +1,11 @@
- net6.0;net7.0;net8.0
+ net8.0
enable
enable
true
12
- Christoph Bühler, smartive AG
+ Christoph Bühler
cbuehler
true
diff --git a/src/Zitadel/Api/Clients.cs b/src/Zitadel/Api/Clients.cs
index c25fb6e9..1367dfe0 100644
--- a/src/Zitadel/Api/Clients.cs
+++ b/src/Zitadel/Api/Clients.cs
@@ -5,17 +5,18 @@
using Zitadel.Auth.V1;
using Zitadel.Authentication;
using Zitadel.Management.V1;
-using Zitadel.Oidc.V2beta;
-using Zitadel.Org.V2beta;
-using Zitadel.Session.V2beta;
-using Zitadel.Settings.V2beta;
+using Zitadel.Oidc.V2;
+using Zitadel.Org.V2;
+using Zitadel.Session.V2;
+using Zitadel.Settings.V2;
using Zitadel.System.V1;
-using Zitadel.User.V2beta;
+using Zitadel.User.V2;
namespace Zitadel.Api;
///
/// Helper class to instantiate (gRPC) api service clients for the ZITADEL API with correct settings.
+/// All other versions are still available, but the latest version is used by default.
///
public static class Clients
{
diff --git a/src/Zitadel/Authentication/Events/Context/LocalFakeZitadelAuthContext.cs b/src/Zitadel/Authentication/Events/Context/LocalFakeZitadelAuthContext.cs
index d67c01f3..a8555560 100644
--- a/src/Zitadel/Authentication/Events/Context/LocalFakeZitadelAuthContext.cs
+++ b/src/Zitadel/Authentication/Events/Context/LocalFakeZitadelAuthContext.cs
@@ -1,91 +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;
- }
- }
-}
+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
index 2aaed257..6fa54962 100644
--- a/src/Zitadel/Authentication/Events/LocalFakeZitadelEvents.cs
+++ b/src/Zitadel/Authentication/Events/LocalFakeZitadelEvents.cs
@@ -1,12 +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;
-}
+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 00f4c2f3..b9b3a456 100644
--- a/src/Zitadel/Authentication/Handler/LocalFakeZitadelHandler.cs
+++ b/src/Zitadel/Authentication/Handler/LocalFakeZitadelHandler.cs
@@ -1,55 +1,47 @@
-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)));
- }
-}
+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;
+
+internal class LocalFakeZitadelHandler(
+ IOptionsMonitor options,
+ ILoggerFactory logger,
+ UrlEncoder encoder)
+ : AuthenticationHandler(options, logger, encoder)
+{
+ 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);
+
+ 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 31e488a5..cb5cfd5c 100644
--- a/src/Zitadel/Authentication/Options/LocalFakeZitadelOptions.cs
+++ b/src/Zitadel/Authentication/Options/LocalFakeZitadelOptions.cs
@@ -1,61 +1,60 @@
-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;
- }
- }
-}
+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();
+
+ ///
+ /// 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/src/Zitadel/Authentication/Options/LocalFakeZitadelSchemeOptions.cs b/src/Zitadel/Authentication/Options/LocalFakeZitadelSchemeOptions.cs
index e8925a92..fd05d193 100644
--- a/src/Zitadel/Authentication/Options/LocalFakeZitadelSchemeOptions.cs
+++ b/src/Zitadel/Authentication/Options/LocalFakeZitadelSchemeOptions.cs
@@ -1,9 +1,8 @@
using Microsoft.AspNetCore.Authentication;
-namespace Zitadel.Authentication.Options
+namespace Zitadel.Authentication.Options;
+
+internal class LocalFakeZitadelSchemeOptions : AuthenticationSchemeOptions
{
- internal class LocalFakeZitadelSchemeOptions : AuthenticationSchemeOptions
- {
- public LocalFakeZitadelOptions FakeZitadelOptions { get; set; } = new();
- }
+ public LocalFakeZitadelOptions FakeZitadelOptions { get; set; } = new();
}
diff --git a/src/Zitadel/Authentication/ZitadelDefaults.cs b/src/Zitadel/Authentication/ZitadelDefaults.cs
index 47ff43c8..c1c03c26 100644
--- a/src/Zitadel/Authentication/ZitadelDefaults.cs
+++ b/src/Zitadel/Authentication/ZitadelDefaults.cs
@@ -1,47 +1,42 @@
-using System.Security.Claims;
+namespace Zitadel.Authentication;
-using Zitadel.Extensions;
-
-namespace Zitadel.Authentication
+///
+/// A set of default values for Zitadel themed authentication/authorization.
+///
+public static class ZitadelDefaults
{
///
- /// A set of default values for Zitadel themed authentication/authorization.
+ /// Default display name.
///
- public static class ZitadelDefaults
- {
- ///
- /// Default display name.
- ///
- public const string DisplayName = "ZITADEL";
+ public const string DisplayName = "ZITADEL";
- ///
- /// Default display name of the fake handler.
- ///
- public const string FakeDisplayName = "ZITADEL-Fake";
+ ///
+ /// Default display name of the fake handler.
+ ///
+ public const string FakeDisplayName = "ZITADEL-Fake";
- ///
- /// Default authentication scheme name for AddZitadel().
- ///
- public const string AuthenticationScheme = "ZITADEL";
+ ///
+ /// Default authentication scheme name for AddZitadel().
+ ///
+ public const string AuthenticationScheme = "ZITADEL";
- ///
- /// Authentication scheme name for local fake provider.
- ///
- public const string FakeAuthenticationScheme = "ZITADEL-Fake";
+ ///
+ /// Authentication scheme name for local fake provider.
+ ///
+ public const string FakeAuthenticationScheme = "ZITADEL-Fake";
- ///
- /// Default callback path for local login redirection.
- ///
- public const string CallbackPath = "/signin-zitadel";
+ ///
+ /// Default callback path for local login redirection.
+ ///
+ public const string CallbackPath = "/signin-zitadel";
- ///
- /// Path to the well-known endpoint of the OIDC config.
- ///
- public const string DiscoveryEndpointPath = "/.well-known/openid-configuration";
+ ///
+ /// Path to the well-known endpoint of the OIDC config.
+ ///
+ public const string DiscoveryEndpointPath = "/.well-known/openid-configuration";
- ///
- /// Header which is used to provide context to grpc/rest api calls.
- ///
- public const string ZitadelOrgIdHeader = "x-zitadel-orgid";
- }
+ ///
+ /// Header which is used to provide context to grpc/rest api calls.
+ ///
+ public const string ZitadelOrgIdHeader = "x-zitadel-orgid";
}
diff --git a/src/Zitadel/Credentials/Application.cs b/src/Zitadel/Credentials/Application.cs
index 26b1f5c4..74855e12 100644
--- a/src/Zitadel/Credentials/Application.cs
+++ b/src/Zitadel/Credentials/Application.cs
@@ -155,10 +155,7 @@ public async Task GetSignedJwtAsync(string audience, TimeSpan? lifeSpan
},
rsa,
JwsAlgorithm.RS256,
- new Dictionary
- {
- { "kid", KeyId },
- });
+ new Dictionary { { "kid", KeyId }, });
}
private async Task GetRsaParametersAsync()
diff --git a/src/Zitadel/Credentials/ServiceAccount.cs b/src/Zitadel/Credentials/ServiceAccount.cs
index d077bc3d..7a35639a 100644
--- a/src/Zitadel/Credentials/ServiceAccount.cs
+++ b/src/Zitadel/Credentials/ServiceAccount.cs
@@ -18,276 +18,275 @@
using Zitadel.Authentication;
-namespace Zitadel.Credentials
+namespace Zitadel.Credentials;
+
+///
+///
+/// A ZITADEL can be loaded from a json file
+/// and helps with authentication on a ZITADEL IAM.
+///
+///
+/// The mechanism is defined here:
+/// JSON Web Token (JWT) Profile.
+/// Create a JWT and sigh it with the private key.
+///
+///
+public record ServiceAccount
{
///
- ///
- /// A ZITADEL can be loaded from a json file
- /// and helps with authentication on a ZITADEL IAM.
- ///
- ///
- /// The mechanism is defined here:
- /// JSON Web Token (JWT) Profile.
- /// Create a JWT and sigh it with the private key.
- ///
+ /// The key type.
///
- public record ServiceAccount
- {
- ///
- /// The key type.
- ///
- public const string Type = "serviceaccount";
+ public const string Type = "serviceaccount";
- private static readonly HttpClient HttpClient = new();
+ private static readonly HttpClient HttpClient = new();
- ///
- /// The user id associated with this service account.
- ///
- public string UserId { get; init; } = string.Empty;
+ ///
+ /// The user id associated with this service account.
+ ///
+ public string UserId { get; init; } = string.Empty;
- ///
- /// This is unique ID (on ZITADEL) of the key.
- ///
- public string KeyId { get; init; } = string.Empty;
+ ///
+ /// This is unique ID (on ZITADEL) of the key.
+ ///
+ public string KeyId { get; init; } = string.Empty;
- ///
- /// The private key generated by ZITADEL for this .
- ///
- public string Key { get; init; } = string.Empty;
+ ///
+ /// The private key generated by ZITADEL for this .
+ ///
+ public string Key { get; init; } = string.Empty;
- ///
- /// Load a from a file at a given (relative or absolute) path.
- ///
- /// The relative or absolute filepath to the json file.
- /// The parsed .
- /// When the file does not exist.
- /// When the deserializer returns 'null'.
- ///
- /// Thrown when the JSON is invalid,
- /// the type is not compatible with the JSON,
- /// or when there is remaining data in the Stream.
- ///
- public static async Task LoadFromJsonFileAsync(string pathToJson)
+ ///
+ /// Load a from a file at a given (relative or absolute) path.
+ ///
+ /// The relative or absolute filepath to the json file.
+ /// The parsed .
+ /// When the file does not exist.
+ /// When the deserializer returns 'null'.
+ ///
+ /// Thrown when the JSON is invalid,
+ /// the type is not compatible with the JSON,
+ /// or when there is remaining data in the Stream.
+ ///
+ public static async Task LoadFromJsonFileAsync(string pathToJson)
+ {
+ var path = Path.GetFullPath(
+ Path.IsPathRooted(pathToJson)
+ ? pathToJson
+ : Path.Join(Directory.GetCurrentDirectory(), pathToJson));
+
+ if (!File.Exists(path))
{
- var path = Path.GetFullPath(
- Path.IsPathRooted(pathToJson)
- ? pathToJson
- : Path.Join(Directory.GetCurrentDirectory(), pathToJson));
+ throw new FileNotFoundException($"File not found: {path}.", path);
+ }
- if (!File.Exists(path))
- {
- throw new FileNotFoundException($"File not found: {path}.", path);
- }
+ await using var stream = File.OpenRead(path);
+ return await LoadFromJsonStreamAsync(stream);
+ }
- await using var stream = File.OpenRead(path);
- return await LoadFromJsonStreamAsync(stream);
- }
+ ///
+ public static ServiceAccount LoadFromJsonFile(string pathToJson) => LoadFromJsonFileAsync(pathToJson).Result;
- ///
- public static ServiceAccount LoadFromJsonFile(string pathToJson) => LoadFromJsonFileAsync(pathToJson).Result;
+ ///
+ /// Load a from a given stream (FileStream, MemoryStream, ...).
+ ///
+ /// The stream to read the json from.
+ /// The parsed .
+ /// When the deserializer returns 'null'.
+ ///
+ /// Thrown when the JSON is invalid,
+ /// the type is not compatible with the JSON,
+ /// or when there is remaining data in the Stream.
+ ///
+ public static async Task LoadFromJsonStreamAsync(Stream stream) =>
+ await JsonSerializer.DeserializeAsync(
+ stream,
+ new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }) ??
+ throw new InvalidDataException("The json file yielded a 'null' result for deserialization.");
+
+ ///
+ public static ServiceAccount LoadFromJsonStream(Stream stream) => LoadFromJsonStreamAsync(stream).Result;
- ///
- /// Load a from a given stream (FileStream, MemoryStream, ...).
- ///
- /// The stream to read the json from.
- /// The parsed .
- /// When the deserializer returns 'null'.
- ///
- /// Thrown when the JSON is invalid,
- /// the type is not compatible with the JSON,
- /// or when there is remaining data in the Stream.
- ///
- public static async Task LoadFromJsonStreamAsync(Stream stream) =>
- await JsonSerializer.DeserializeAsync(
- stream,
- new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }) ??
- throw new InvalidDataException("The json file yielded a 'null' result for deserialization.");
-
- ///
- public static ServiceAccount LoadFromJsonStream(Stream stream) => LoadFromJsonStreamAsync(stream).Result;
+ ///
+ /// Load a from a string that contains json.
+ ///
+ /// Json string.
+ /// The parsed .
+ /// When the deserializer returns 'null'.
+ ///
+ /// Thrown when the JSON is invalid,
+ /// the type is not compatible with the JSON,
+ /// or when there is remaining data in the Stream.
+ ///
+ public static async Task LoadFromJsonStringAsync(string json)
+ {
+ await using var memoryStream = new MemoryStream(Encoding.UTF8.GetBytes(json), 0, json.Length);
+ return await LoadFromJsonStreamAsync(memoryStream);
+ }
- ///
- /// Load a from a string that contains json.
- ///
- /// Json string.
- /// The parsed .
- /// When the deserializer returns 'null'.
- ///
- /// Thrown when the JSON is invalid,
- /// the type is not compatible with the JSON,
- /// or when there is remaining data in the Stream.
- ///
- public static async Task LoadFromJsonStringAsync(string json)
+ ///
+ public static ServiceAccount LoadFromJsonString(string json) => LoadFromJsonStringAsync(json).Result;
+
+ ///
+ /// Authenticate the given service account against the issuer in the options.
+ /// As an example, the received token can be used to communicate with API applications or
+ /// with the ZITADEL API itself.
+ ///
+ /// The audience to authenticate against. Typically, this is a ZITADEL URL.
+ /// that contain the parameters for the authentication process.
+ /// An opaque access token which can be used to communicate with relying parties.
+ public async Task AuthenticateAsync(string audience, AuthOptions? authOptions = null)
+ {
+ authOptions ??= new();
+ var manager = new ConfigurationManager(
+ authOptions.DiscoveryEndpoint ?? GetDiscoveryEndpoint(audience),
+ new OpenIdConnectConfigurationRetriever(),
+ new HttpDocumentRetriever(HttpClient) { RequireHttps = authOptions.RequireHttps ?? true });
+
+ var oidcConfig = await manager.GetConfigurationAsync();
+
+ var jwt = await GetSignedJwtAsync(audience);
+ var request = new HttpRequestMessage(HttpMethod.Post, oidcConfig.TokenEndpoint)
{
- await using var memoryStream = new MemoryStream(Encoding.UTF8.GetBytes(json), 0, json.Length);
- return await LoadFromJsonStreamAsync(memoryStream);
- }
+ Content = new FormUrlEncodedContent(
+ new[]
+ {
+ new KeyValuePair("grant_type", "urn:ietf:params:oauth:grant-type:jwt-bearer"),
+ new KeyValuePair(
+ "assertion",
+ $"{jwt}"),
+ new KeyValuePair("scope", authOptions.CreateOidcScopes()),
+ }),
+ };
- ///
- public static ServiceAccount LoadFromJsonString(string json) => LoadFromJsonStringAsync(json).Result;
+ var response = await HttpClient.SendAsync(request);
- ///
- /// Authenticate the given service account against the issuer in the options.
- /// As an example, the received token can be used to communicate with API applications or
- /// with the ZITADEL API itself.
- ///
- /// The audience to authenticate against. Typically, this is a ZITADEL URL.
- /// that contain the parameters for the authentication process.
- /// An opaque access token which can be used to communicate with relying parties.
- public async Task AuthenticateAsync(string audience, AuthOptions? authOptions = null)
+ try
+ {
+ var token = await response
+ .EnsureSuccessStatusCode()
+ .Content
+ .ReadFromJsonAsync();
+ return token?.AccessToken ?? throw new AuthenticationException("Access token could not be parsed.");
+ }
+ catch (HttpRequestException e)
{
- authOptions ??= new();
- var manager = new ConfigurationManager(
- authOptions.DiscoveryEndpoint ?? GetDiscoveryEndpoint(audience),
- new OpenIdConnectConfigurationRetriever(),
- new HttpDocumentRetriever(HttpClient) { RequireHttps = authOptions.RequireHttps ?? true });
+ var err = await response.Content.ReadAsStringAsync();
+ throw new HttpRequestException(err, e);
+ }
+ }
- var oidcConfig = await manager.GetConfigurationAsync();
+ ///
+ public string Authenticate(string audience, AuthOptions? authOptions = null) =>
+ AuthenticateAsync(audience, authOptions).Result;
- var jwt = await GetSignedJwtAsync(audience);
- var request = new HttpRequestMessage(HttpMethod.Post, oidcConfig.TokenEndpoint)
- {
- Content = new FormUrlEncodedContent(
- new[]
- {
- new KeyValuePair("grant_type", "urn:ietf:params:oauth:grant-type:jwt-bearer"),
- new KeyValuePair(
- "assertion",
- $"{jwt}"),
- new KeyValuePair("scope", authOptions.CreateOidcScopes()),
- }),
- };
+ private static string GetDiscoveryEndpoint(string discoveryEndpoint) =>
+ discoveryEndpoint.EndsWith(ZitadelDefaults.DiscoveryEndpointPath)
+ ? discoveryEndpoint
+ : discoveryEndpoint.TrimEnd('/') + ZitadelDefaults.DiscoveryEndpointPath;
- var response = await HttpClient.SendAsync(request);
+ [SuppressMessage(
+ "Critical Vulnerability",
+ "S4426:Cryptographic keys should be robust",
+ Justification = "The key is loaded from a file and is not generated by the application.")]
+ private async Task GetSignedJwtAsync(string audience)
+ {
+ using var rsa = new RSACryptoServiceProvider();
+ rsa.ImportParameters(await GetRsaParametersAsync());
- try
- {
- var token = await response
- .EnsureSuccessStatusCode()
- .Content
- .ReadFromJsonAsync();
- return token?.AccessToken ?? throw new AuthenticationException("Access token could not be parsed.");
- }
- catch (HttpRequestException e)
+ return JWT.Encode(
+ new Dictionary
{
- var err = await response.Content.ReadAsStringAsync();
- throw new HttpRequestException(err, e);
- }
+ { "iss", UserId },
+ { "sub", UserId },
+ { "iat", DateTimeOffset.UtcNow.AddSeconds(-1).ToUnixTimeSeconds() },
+ { "exp", DateTimeOffset.UtcNow.AddMinutes(1).ToUnixTimeSeconds() },
+ { "aud", audience },
+ },
+ rsa,
+ JwsAlgorithm.RS256,
+ new Dictionary { { "kid", KeyId }, });
+ }
+
+ private async Task GetRsaParametersAsync()
+ {
+ var bytes = Encoding.UTF8.GetBytes(Key);
+ await using var ms = new MemoryStream(bytes);
+ using var sr = new StreamReader(ms);
+ var pemReader = new PemReader(sr);
+
+ if (pemReader.ReadObject() is not AsymmetricCipherKeyPair keyPair)
+ {
+ throw new AuthenticationException("RSA Keypair could not be read.");
}
- ///
- public string Authenticate(string audience, AuthOptions? authOptions = null) =>
- AuthenticateAsync(audience, authOptions).Result;
+ return DotNetUtilities.ToRSAParameters(keyPair.Private as RsaPrivateCrtKeyParameters);
+ }
- private static string GetDiscoveryEndpoint(string discoveryEndpoint) =>
- discoveryEndpoint.EndsWith(ZitadelDefaults.DiscoveryEndpointPath)
- ? discoveryEndpoint
- : discoveryEndpoint.TrimEnd('/') + ZitadelDefaults.DiscoveryEndpointPath;
+ ///
+ /// Options for the authentication with a .
+ ///
+ public record AuthOptions
+ {
+ ///
+ /// Scope that can be added to signal that the ZITADEL API needs to be in the audience of the token.
+ /// This replaces the need for the "project id" of the ZITADEL API project to be present.
+ ///
+ public const string ApiAccessScope = "urn:zitadel:iam:org:project:id:zitadel:aud";
- [SuppressMessage(
- "Critical Vulnerability",
- "S4426:Cryptographic keys should be robust",
- Justification = "The key is loaded from a file and is not generated by the application.")]
- private async Task GetSignedJwtAsync(string audience)
- {
- using var rsa = new RSACryptoServiceProvider();
- rsa.ImportParameters(await GetRsaParametersAsync());
+ ///
+ /// If set, the requested access token from ZITADEL will include the "ZITADEL API" project
+ /// in its audience. The returned token will be able to access the API on the service accounts
+ /// behalf.
+ ///
+ public bool ApiAccess { get; init; }
- return JWT.Encode(
- new Dictionary
- {
- { "iss", UserId },
- { "sub", UserId },
- { "iat", DateTimeOffset.UtcNow.AddSeconds(-1).ToUnixTimeSeconds() },
- { "exp", DateTimeOffset.UtcNow.AddMinutes(1).ToUnixTimeSeconds() },
- { "aud", audience },
- },
- rsa,
- JwsAlgorithm.RS256,
- new Dictionary { { "kid", KeyId }, });
- }
+ ///
+ /// If set, overwrites the discovery endpoint for the audience in the authentication.
+ /// This may be used, if the discovery endpoint is not on the well-known url
+ /// of the endpoint.
+ ///
+ public string? DiscoveryEndpoint { get; init; }
- private async Task GetRsaParametersAsync()
- {
- var bytes = Encoding.UTF8.GetBytes(Key);
- await using var ms = new MemoryStream(bytes);
- using var sr = new StreamReader(ms);
- var pemReader = new PemReader(sr);
+ ///
+ /// Requires Https secure channel for sending requests. This is turned ON by default for security reasons. It is RECOMMENDED that you do not allow retrieval from http addresses by default.
+ ///
+ public bool? RequireHttps { get; init; }
- if (pemReader.ReadObject() is not AsymmetricCipherKeyPair keyPair)
- {
- throw new AuthenticationException("RSA Keypair could not be read.");
- }
+ ///
+ /// Set a list of roles that must be attached to this service account to be
+ /// successfully authenticated. Translates to the role scope ("urn:zitadel:iam:org:project:role:{Role}").
+ ///
+ public IList RequiredRoles { get; init; } = new List();
- return DotNetUtilities.ToRSAParameters(keyPair.Private as RsaPrivateCrtKeyParameters);
- }
+ ///
+ /// Set a list of audiences that are attached to the returned access token.
+ /// Translates to the additional audience scope ("urn:zitadel:iam:org:project:id:{ProjectId}:aud").
+ ///
+ public IList ProjectAudiences { get; init; } = new List();
///
- /// Options for the authentication with a .
+ /// List of arbitrary additional scopes that are concatenated into the scope.
///
- public record AuthOptions
- {
- ///
- /// Scope that can be added to signal that the ZITADEL API needs to be in the audience of the token.
- /// This replaces the need for the "project id" of the ZITADEL API project to be present.
- ///
- public const string ApiAccessScope = "urn:zitadel:iam:org:project:id:zitadel:aud";
-
- ///
- /// If set, the requested access token from ZITADEL will include the "ZITADEL API" project
- /// in its audience. The returned token will be able to access the API on the service accounts
- /// behalf.
- ///
- public bool ApiAccess { get; init; }
-
- ///
- /// If set, overwrites the discovery endpoint for the audience in the authentication.
- /// This may be used, if the discovery endpoint is not on the well-known url
- /// of the endpoint.
- ///
- public string? DiscoveryEndpoint { get; init; }
-
- ///
- /// Requires Https secure channel for sending requests. This is turned ON by default for security reasons. It is RECOMMENDED that you do not allow retrieval from http addresses by default.
- ///
- public bool? RequireHttps { get; init; }
-
- ///
- /// Set a list of roles that must be attached to this service account to be
- /// successfully authenticated. Translates to the role scope ("urn:zitadel:iam:org:project:role:{Role}").
- ///
- public IList RequiredRoles { get; init; } = new List();
-
- ///
- /// Set a list of audiences that are attached to the returned access token.
- /// Translates to the additional audience scope ("urn:zitadel:iam:org:project:id:{ProjectId}:aud").
- ///
- public IList ProjectAudiences { get; init; } = new List();
-
- ///
- /// List of arbitrary additional scopes that are concatenated into the scope.
- ///
- public IList AdditionalScopes { get; init; } = new List();
-
- internal string CreateOidcScopes() =>
- string.Join(
- ' ',
- new[]
- {
- "openid", ApiAccess
- ? ApiAccessScope
- : string.Empty,
- }
- .Union(AdditionalScopes)
- .Union(ProjectAudiences.Select(p => $"urn:zitadel:iam:org:project:id:{p}:aud"))
- .Union(RequiredRoles.Select(r => $"urn:zitadel:iam:org:project:role:{r}"))
- .Where(s => !string.IsNullOrWhiteSpace(s)));
- }
+ public IList AdditionalScopes { get; init; } = new List();
- private sealed record AccessTokenResponse
- {
- [JsonPropertyName("access_token")]
- public string AccessToken { get; init; } = string.Empty;
- }
+ internal string CreateOidcScopes() =>
+ string.Join(
+ ' ',
+ new[]
+ {
+ "openid", ApiAccess
+ ? ApiAccessScope
+ : string.Empty,
+ }
+ .Union(AdditionalScopes)
+ .Union(ProjectAudiences.Select(p => $"urn:zitadel:iam:org:project:id:{p}:aud"))
+ .Union(RequiredRoles.Select(r => $"urn:zitadel:iam:org:project:role:{r}"))
+ .Where(s => !string.IsNullOrWhiteSpace(s)));
+ }
+
+ private sealed record AccessTokenResponse
+ {
+ [JsonPropertyName("access_token")]
+ public string AccessToken { get; init; } = string.Empty;
}
}
diff --git a/src/Zitadel/Zitadel.csproj b/src/Zitadel/Zitadel.csproj
index a5ba61ec..5e991ed4 100644
--- a/src/Zitadel/Zitadel.csproj
+++ b/src/Zitadel/Zitadel.csproj
@@ -26,16 +26,6 @@
-
-
-
-
-
-
-
-
-
-
diff --git a/tests/Zitadel.Test/Authentication/ZitadelFakeAuthenticationHandler.Test.cs b/tests/Zitadel.Test/Authentication/ZitadelFakeAuthenticationHandler.Test.cs
index 8312e26c..cff5a101 100644
--- a/tests/Zitadel.Test/Authentication/ZitadelFakeAuthenticationHandler.Test.cs
+++ b/tests/Zitadel.Test/Authentication/ZitadelFakeAuthenticationHandler.Test.cs
@@ -1,67 +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");
- }
-
- [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");
- }
-}
+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/Extensions/ClaimsPrincipalExtensionsTest.cs b/tests/Zitadel.Test/Extensions/ClaimsPrincipalExtensionsTest.cs
index 5e04b17e..f71e8b4f 100644
--- a/tests/Zitadel.Test/Extensions/ClaimsPrincipalExtensionsTest.cs
+++ b/tests/Zitadel.Test/Extensions/ClaimsPrincipalExtensionsTest.cs
@@ -53,7 +53,8 @@ public void IsNotInRole()
[Fact]
public void IsNotInNoneOfTheGivenRoles()
{
- bool actual = ClaimsPrincipalExtensions.IsInRole(claimsPrincipal.Object, new[] { "negative", "negative", "negative" });
+ bool actual =
+ ClaimsPrincipalExtensions.IsInRole(claimsPrincipal.Object, new[] { "negative", "negative", "negative" });
Assert.False(actual);
claimsPrincipal.Verify(c => c.IsInRole("negative"), Times.Exactly(3));
diff --git a/tests/Zitadel.Test/WebFactories/AuthenticationHandlerWebFactory.cs b/tests/Zitadel.Test/WebFactories/AuthenticationHandlerWebFactory.cs
index 3e25cc26..1cd64a3a 100644
--- a/tests/Zitadel.Test/WebFactories/AuthenticationHandlerWebFactory.cs
+++ b/tests/Zitadel.Test/WebFactories/AuthenticationHandlerWebFactory.cs
@@ -1,7 +1,4 @@
-using System.Collections.Generic;
-using System.IO;
-using System.Linq;
-using System.Security.Claims;
+using System.Security.Claims;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
@@ -11,7 +8,6 @@
using Microsoft.Extensions.Hosting;
using Zitadel.Authentication;
-using Zitadel.Credentials;
using Zitadel.Extensions;
namespace Zitadel.Test.WebFactories;
diff --git a/tests/Zitadel.Test/WebFactories/FakeAuthenticationHandlerWebFactory.cs b/tests/Zitadel.Test/WebFactories/FakeAuthenticationHandlerWebFactory.cs
index 36e74c21..91f4a71f 100644
--- a/tests/Zitadel.Test/WebFactories/FakeAuthenticationHandlerWebFactory.cs
+++ b/tests/Zitadel.Test/WebFactories/FakeAuthenticationHandlerWebFactory.cs
@@ -1,117 +1,114 @@
-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
-}
+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
+}