From 09346e02176a9ab0abef7e8069159d07955cd91e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christoph=20B=C3=BChler?= Date: Mon, 28 Oct 2024 10:51:47 +0100 Subject: [PATCH] feat: upgrade Zitadel Libs and remove old .net version (#933) BREAKING CHANGE: change Zitadel default client mapping to V2 instead of V2Beta. Older APIs are still generated but the default helpers will create V2 clients. BREAKING CHANGE: remove .Net 6 and 7 since they are end of support. --- .github/workflows/dotnet-test.yml | 2 - .../Zitadel.ApiAccess.csproj | 2 +- .../Pages/Authenticated.cshtml | 1 - .../Pages/Authenticated.cshtml.cs | 5 +- .../Pages/LoggedOut.cshtml | 2 +- .../Pages/LoggedOut.cshtml.cs | 12 +- examples/Zitadel.AspNet.AuthN/Program.cs | 2 +- .../Zitadel.AspNet.AuthN.csproj | 2 +- examples/Zitadel.WebApi/Program.cs | 3 +- examples/Zitadel.WebApi/Zitadel.WebApi.csproj | 2 +- src/Directory.Build.props | 4 +- src/Zitadel/Api/Clients.cs | 11 +- .../Context/LocalFakeZitadelAuthContext.cs | 182 +++---- .../Events/LocalFakeZitadelEvents.cs | 24 +- .../Handler/LocalFakeZitadelHandler.cs | 102 ++-- .../Options/LocalFakeZitadelOptions.cs | 121 +++-- .../Options/LocalFakeZitadelSchemeOptions.cs | 9 +- src/Zitadel/Authentication/ZitadelDefaults.cs | 67 ++- src/Zitadel/Credentials/Application.cs | 5 +- src/Zitadel/Credentials/ServiceAccount.cs | 471 +++++++++--------- src/Zitadel/Zitadel.csproj | 10 - .../ZitadelFakeAuthenticationHandler.Test.cs | 134 ++--- .../ClaimsPrincipalExtensionsTest.cs | 3 +- .../AuthenticationHandlerWebFactory.cs | 6 +- .../FakeAuthenticationHandlerWebFactory.cs | 231 +++++---- 25 files changed, 688 insertions(+), 725 deletions(-) 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 +}