Skip to content

Commit

Permalink
feat(auth): Add local fake authentication for testing. (#10)
Browse files Browse the repository at this point in the history
This closes #9.

This adds an `AddFakeZitadel` method extension to
the authentication builder. With this local fake,
one can bypass authentication for development.
To fail a specific request, add the "x-zitadel-fake-auth"
header with the value "false" to the request.

Signed-off-by: Christoph Bühler <christoph@smartive.ch>
  • Loading branch information
buehler authored Jan 6, 2021
1 parent 56e8d0d commit 881f79f
Show file tree
Hide file tree
Showing 8 changed files with 320 additions and 5 deletions.
37 changes: 37 additions & 0 deletions src/Zitadel/Authentication/AuthenticationBuilderExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.IdentityModel.Tokens;
using Zitadel.Authentication.Handler;
using Zitadel.Authentication.Options;
using Zitadel.Authentication.Validation;

Expand Down Expand Up @@ -265,5 +266,41 @@ public static AuthenticationBuilder AddZitadelAuthenticationHandler(
zitadelOptions.DiscoveryEndpoint,
zitadelOptions.PrimaryDomain));
});

/// <summary>
/// Add a "fake" zitadel authentication. This should only be used for local
/// development to fake an authentication/authorization. All calls are authenticated
/// by default. If (e.g. for testing reasons) a specific call should NOT be authenticated,
/// attach the header "x-zitadel-fake-auth" with the value "false" to the request.
/// This specific request will then fail to authenticate.
/// </summary>
/// <param name="builder">The <see cref="AuthenticationBuilder"/> to configure.</param>
/// <param name="configureOptions">Action to configure the <see cref="LocalFakeZitadelOptions"/>.</param>
/// <returns>The configured <see cref="AuthenticationBuilder"/>.</returns>
public static AuthenticationBuilder AddFakeZitadel(
this AuthenticationBuilder builder,
Action<LocalFakeZitadelOptions>? configureOptions)
{
var options = new LocalFakeZitadelOptions();
configureOptions?.Invoke(options);
return builder.AddFakeZitadel(options);
}

/// <summary>
/// Add a "fake" zitadel authentication. This should only be used for local
/// development to fake an authentication/authorization. All calls are authenticated
/// by default. If (e.g. for testing reasons) a specific call should NOT be authenticated,
/// attach the header "x-zitadel-fake-auth" with the value "false" to the request.
/// This specific request will then fail to authenticate.
/// </summary>
/// <param name="builder">The <see cref="AuthenticationBuilder"/> to configure.</param>
/// <param name="options">The <see cref="LocalFakeZitadelOptions"/> to use.</param>
/// <returns>The configured <see cref="AuthenticationBuilder"/>.</returns>
public static AuthenticationBuilder AddFakeZitadel(
this AuthenticationBuilder builder,
LocalFakeZitadelOptions options)
=> builder.AddScheme<LocalFakeZitadelSchemeOptions, LocalFakeZitadelHandler>(
ZitadelDefaults.FakeAuthenticationScheme,
o => o.FakeZitadelOptions = options);
}
}
47 changes: 47 additions & 0 deletions src/Zitadel/Authentication/Handler/LocalFakeZitadelHandler.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
using System.Collections.Generic;
using System.Linq;
using System.Security.Claims;
using System.Text.Encodings.Web;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authentication;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Zitadel.Authentication.Options;

namespace Zitadel.Authentication.Handler
{
internal class LocalFakeZitadelHandler : AuthenticationHandler<LocalFakeZitadelSchemeOptions>
{
private const string FakeAuthHeader = "x-zitadel-fake-auth";

public LocalFakeZitadelHandler(
IOptionsMonitor<LocalFakeZitadelSchemeOptions> options,
ILoggerFactory logger,
UrlEncoder encoder,
ISystemClock clock)
: base(options, logger, encoder, clock)
{
}

protected override Task<AuthenticateResult> 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 claims = new List<Claim>
{
new(ClaimTypes.NameIdentifier, Options.FakeZitadelOptions.FakeZitadelId),
new("sub", Options.FakeZitadelOptions.FakeZitadelId),
}.Concat(Options.FakeZitadelOptions.AdditionalClaims)
.Concat(Options.FakeZitadelOptions.Roles.Select(r => new Claim(ClaimTypes.Role, r)));

var identity = new ClaimsIdentity(claims, ZitadelDefaults.FakeAuthenticationScheme);

return Task.FromResult(
AuthenticateResult.Success(
new AuthenticationTicket(new ClaimsPrincipal(identity), ZitadelDefaults.FakeAuthenticationScheme)));
}
}
}
55 changes: 55 additions & 0 deletions src/Zitadel/Authentication/Options/LocalFakeZitadelOptions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
using System.Collections.Generic;
using System.Security.Claims;

namespace Zitadel.Authentication.Options
{
public class LocalFakeZitadelOptions
{
/// <summary>
/// The "user-id" of the fake user.
/// This populates the "sub" and "nameidentifier" claims.
/// </summary>
public string FakeZitadelId { get; set; } = string.Empty;

/// <summary>
/// A list of additional claims to add to the identity.
/// </summary>
public IList<Claim> AdditionalClaims { get; set; } = new List<Claim>();

/// <summary>
/// List of roles that are attached to the identity.
/// Note: the roles are actually "claims" but this list exists
/// for convenience.
/// </summary>
public IEnumerable<string> Roles { get; set; } = new List<string>();

/// <summary>
/// Add a claim to the <see cref="AdditionalClaims"/> list.
/// This is a convenience method for modifying <see cref="AdditionalClaims"/>.
/// </summary>
/// <param name="type">Type of the claim (examples: <see cref="ClaimTypes"/>).</param>
/// <param name="value">The value.</param>
/// <param name="valueType">Type of the value (examples: <see cref="ClaimValueTypes"/>).</param>
/// <param name="issuer">The issuer for this claim.</param>
/// <param name="originalIssuer">The original issuer of this claim.</param>
/// <returns>The <see cref="LocalFakeZitadelOptions"/> for chaining.</returns>
public LocalFakeZitadelOptions AddClaim(
string type,
string value,
string? valueType,
string? issuer,
string? originalIssuer) => AddClaim(new(type, value, valueType, issuer, originalIssuer));

/// <summary>
/// Add a claim to the <see cref="AdditionalClaims"/> list.
/// This is a convenience method for modifying <see cref="AdditionalClaims"/>.
/// </summary>
/// <param name="claim">The claim to add.</param>
/// <returns>The <see cref="LocalFakeZitadelOptions"/> for chaining.</returns>
public LocalFakeZitadelOptions AddClaim(Claim claim)
{
AdditionalClaims.Add(claim);
return this;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
using Microsoft.AspNetCore.Authentication;

namespace Zitadel.Authentication.Options
{
internal class LocalFakeZitadelSchemeOptions : AuthenticationSchemeOptions
{
public LocalFakeZitadelOptions FakeZitadelOptions { get; set; } = new();
}
}
5 changes: 5 additions & 0 deletions src/Zitadel/Authentication/ZitadelDefaults.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,11 @@ public static class ZitadelDefaults
/// </summary>
public const string AuthenticationScheme = "Zitadel";

/// <summary>
/// Authentication scheme name for local fake provider.
/// </summary>
public const string FakeAuthenticationScheme = "ZitadelLocalFake";

/// <summary>
/// Default authentication scheme name for AddZitadelAuthenticationHandler().
/// </summary>
Expand Down
10 changes: 5 additions & 5 deletions src/Zitadel/Zitadel.csproj
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">

<Import Project="..\..\config\Common.targets"/>
<Import Project="..\..\config\CodeAnalysis.targets"/>
<Import Project="..\..\config\Common.targets" />
<Import Project="..\..\config\CodeAnalysis.targets" />

<PropertyGroup>
<TargetFramework>net5.0</TargetFramework>
Expand All @@ -28,12 +28,12 @@
</PropertyGroup>

<ItemGroup>
<None Include="icon.png" Pack="true" PackagePath="\" Visible="false"/>
<None Include="icon.png" Pack="true" PackagePath="\" Visible="false" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="5.0.1"/>
<PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="5.0.1"/>
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="5.0.1" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="5.0.1" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
using System.Net.Http;
using System.Net.Http.Json;
using System.Security.Claims;
using System.Threading.Tasks;
using FluentAssertions;
using Microsoft.AspNetCore.Http;
using Xunit;
using Zitadel.Test.WebFactories;

namespace Zitadel.Test.Authentication
{
public class ZitadelFakeAuthenticationHandler : IClassFixture<FakeAuthenticationHandlerWebFactory>
{
private readonly FakeAuthenticationHandlerWebFactory _factory;

public ZitadelFakeAuthenticationHandler(FakeAuthenticationHandlerWebFactory factory)
{
_factory = factory;
}

[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(StatusCodes.Status401Unauthorized);
}

[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("ZitadelLocalFake");
result?.UserId.Should().Be("1234");
result?.Claims.Should().Contain(claim => claim.Key == ClaimTypes.Role && claim.Value == "User");
}
}
}
107 changes: 107 additions & 0 deletions tests/Zitadel.Test/WebFactories/FakeAuthenticationHandlerWebFactory.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
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;

namespace Zitadel.Test.WebFactories
{
public class FakeAuthenticationHandlerWebFactory : WebApplicationFactory<FakeAuthenticationHandlerWebFactory>
{
#region Startup

public void ConfigureServices(IServiceCollection services)
{
services
.AddAuthorization()
.AddAuthentication(ZitadelDefaults.FakeAuthenticationScheme)
.AddFakeZitadel(
options =>
{
options.FakeZitadelId = "1234";
options.AdditionalClaims = new List<Claim>
{
new("foo", "bar"),
};
options.Roles = new List<string> { "User" };
});
}

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
app.UseRouting();

app.UseAuthentication();
app.UseAuthorization();

app.UseEndpoints(
endpoints =>
{
endpoints.MapGet(
"/unauthed",
async context => { await context.Response.WriteAsJsonAsync(new Unauthed { Ping = "Pong" }); });
endpoints.MapGet(
"/authed",
async context =>
{
await context.Response.WriteAsJsonAsync(
new Authed
{
Ping = "Pong",
AuthType = context.User.Identity?.AuthenticationType,
UserId = context.User.FindFirstValue(ClaimTypes.NameIdentifier),
Claims = context.User.Claims.Select(
c => new KeyValuePair<string, string>(c.Type, c.Value))
.ToList(),
});
})
.RequireAuthorization();
});
}

#endregion

#region WebApplicationFactory

protected override IHostBuilder CreateHostBuilder()
=> Host
.CreateDefaultBuilder()
.ConfigureWebHostDefaults(
builder => builder
.UseStartup<FakeAuthenticationHandlerWebFactory>());

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; }
}

internal record Authed
{
public string Ping { get; init; }

public string AuthType { get; init; }

public string UserId { get; init; }

public List<KeyValuePair<string, string>> Claims { get; init; }
}

#endregion
}
}

0 comments on commit 881f79f

Please sign in to comment.