Skip to content

Commit

Permalink
tests: Add Auth configuration tests (#76)
Browse files Browse the repository at this point in the history
  • Loading branch information
NikiforovAll authored Apr 29, 2024
1 parent b52e8f4 commit 7aafd05
Show file tree
Hide file tree
Showing 11 changed files with 215 additions and 41 deletions.
62 changes: 54 additions & 8 deletions docs/configuration/configuration-authentication.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,38 +35,74 @@ The **Keycloak.AuthServices.Authentication** library will automatically retrieve

Simply add:

<<< @/../tests/Keycloak.AuthServices.IntegrationTests/AddKeycloakWebApiAuthenticationTests.cs#AddKeycloakWebApiAuthentication_FromConfiguration
<<< @/../tests/Keycloak.AuthServices.IntegrationTests/ConfigurationTests/AddKeycloakWebApiAuthenticationTests.cs#AddKeycloakWebApiAuthentication_FromConfiguration

This default assumption of the "Keycloak" section allows you to easily configure the library without explicitly specifying the section name every time. However, if you have a different section name or want to customize the configuration retrieval process, the library provides additional methods and options to handle that.

::: code-group
<<< @/../tests/Keycloak.AuthServices.IntegrationTests/AddKeycloakWebApiAuthenticationTests.cs#AddKeycloakWebApiAuthentication_FromConfigurationSection [specify configuration section]
<<< @/../tests/Keycloak.AuthServices.IntegrationTests/ConfigurationTests/AddKeycloakWebApiAuthenticationTests.cs#AddKeycloakWebApiAuthentication_FromConfigurationSection [specify configuration section]

<<< @/../tests/Keycloak.AuthServices.IntegrationTests/AddKeycloakWebApiAuthenticationTests.cs#AddKeycloakWebApiAuthentication_FromConfiguration2 [specify section name]
<<< @/../tests/Keycloak.AuthServices.IntegrationTests/ConfigurationTests/AddKeycloakWebApiAuthenticationTests.cs#AddKeycloakWebApiAuthentication_FromConfiguration2 [specify section name]

:::

Not everything you want to do can be configured with `KeycloakAuthenticationOptions`, for more fine-grained configuration use next method overload that takes `Action<JwtBearerOptions>`:

<<< @/../tests/Keycloak.AuthServices.IntegrationTests/AddKeycloakWebApiAuthenticationTests.cs#AddKeycloakWebApiAuthentication_FromConfigurationWithOverrides
<<< @/../tests/Keycloak.AuthServices.IntegrationTests/ConfigurationTests/AddKeycloakWebApiAuthenticationTests.cs#AddKeycloakWebApiAuthentication_FromConfigurationWithInlineOverrides

Typically, ASP.NET Core expects to find these (default) options under the `Authentication:Schemes:{SchemeName}`. See [Configuring Authentication **Strategies**](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/minimal-apis/security?view=aspnetcore-8.0#configuring-authentication-strategy) for more details. Here is how to configure [JwtBearerOptions](https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.builder.jwtbeareroptions):

<<< @/../tests/Keycloak.AuthServices.IntegrationTests/ConfigurationTests/AddKeycloakWebApiAuthenticationTests.cs#AddKeycloakWebApiAuthentication_FromConfigurationWithOverrides

```json
{
"Keycloak": {
"ssl-required": "internal",
"resource": "test-client",
"verify-token-audience": true,
"credentials": {
"secret": "Tgx4lvbyhho7oNFmiIupDRVA8ioQY7PW"
},
"confidential-port": 0
},
"Authentication": {
"DefaultScheme": "Bearer",
"Schemes": {
"Bearer": {
"ValidAudiences": [
"default-test-client-new"
],
"RequireHttpsMetadata": true,
"Authority": "http://localhost:8080/realms/DefaultTest",
"TokenValidationParameters": {
"ValidateAudience": false
}
}
}
}
}
```

> [!NOTE]
> `KeycloakAuthenticationOptions` ("Keycloak") takes precedence over `Authentication:Schemes:{SchemeName}` ("Bearer") in the case of default configuration
### AuthenticationBuilder Extensions

For situations when you want to override *Authentication Scheme* or you just prefer more verbose way of defining your project's *Authentication* you can use `AuthenticationBuilder` extension methods:

<<< @/../tests/Keycloak.AuthServices.IntegrationTests/AddKeycloakWebApiTests.cs#AddKeycloakWebApiAuthentication_FromConfiguration
<<< @/../tests/Keycloak.AuthServices.IntegrationTests/ConfigurationTests/AddKeycloakWebApiTests.cs#AddKeycloakWebApiAuthentication_FromConfiguration

Use `IConfigurationSection`:

<<< @/../tests/Keycloak.AuthServices.IntegrationTests/AddKeycloakWebApiTests.cs#AddKeycloakWebApiAuthentication_FromConfigurationSection
<<< @/../tests/Keycloak.AuthServices.IntegrationTests/ConfigurationTests/AddKeycloakWebApiTests.cs#AddKeycloakWebApiAuthentication_FromConfigurationSection

Inline declaration:

<<< @/../tests/Keycloak.AuthServices.IntegrationTests/AddKeycloakWebApiTests.cs#AddKeycloakWebApiAuthentication_FromInline
<<< @/../tests/Keycloak.AuthServices.IntegrationTests/ConfigurationTests/AddKeycloakWebApiTests.cs#AddKeycloakWebApiAuthentication_FromInline

Inline declaration with `JwtBearerOptions` overrides:

<<< @/../tests/Keycloak.AuthServices.IntegrationTests/AddKeycloakWebApiTests.cs#AddKeycloakWebApiAuthentication_FromInline2
<<< @/../tests/Keycloak.AuthServices.IntegrationTests/ConfigurationTests/AddKeycloakWebApiTests.cs#AddKeycloakWebApiAuthentication_FromInline2

## Web App

Expand Down Expand Up @@ -163,6 +199,16 @@ Here is an example of **keycloak.json** adapter file:

Keycloak roles can be automatically transformed to [AspNetCore Roles](https://learn.microsoft.com/en-us/aspnet/core/security/authorization/roles). This feature is disabled by default.

Specify `KeycloakAuthenticationOptions.RolesSource` to enable it. E.g.:

```json
{
"Keycloak": {
"RolesSource": "Realm"
}
}
```

There are three options to determine a source for the roles:

```csharp
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -114,8 +114,9 @@ private static void AddKeycloakWebApiImplementation(
builder.Services.AddTransient<IClaimsTransformation>(sp =>
{
var keycloakOptions = sp.GetRequiredService<
IOptions<KeycloakAuthenticationOptions>
>().Value;
IOptionsMonitor<KeycloakAuthenticationOptions>
>()
.Get(jwtBearerScheme);
return new KeycloakRolesClaimsTransformation(
keycloakOptions.RoleClaimType,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -223,8 +223,9 @@ private static void AddKeycloakWebAppInternal(
builder.Services.AddTransient<IClaimsTransformation>(sp =>
{
var keycloakOptions = sp.GetRequiredService<
IOptions<KeycloakAuthenticationOptions>
>().Value;
IOptionsMonitor<KeycloakAuthenticationOptions>
>()
.Get(openIdConnectScheme);
return new KeycloakRolesClaimsTransformation(
keycloakOptions.RoleClaimType,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,15 +34,15 @@ public string AuthServerUrl
/// <summary>
/// Keycloak Realm
/// </summary>
public string Realm { get; set; } = string.Empty;
public string Realm { get; set; } = default!;

/// <summary>
/// Resource as client id
/// </summary>
/// <example>
/// "resource": "client-id"
/// </example>
public string Resource { get; set; } = string.Empty;
public string Resource { get; set; } = default!;

/// <summary>
/// Audience verification
Expand Down
Original file line number Diff line number Diff line change
@@ -1,18 +1,30 @@
namespace Keycloak.AuthServices.IntegrationTests;
namespace Keycloak.AuthServices.IntegrationTests.ConfigurationTests;

using System.Net;
using Alba;
using Keycloak.AuthServices.Authentication;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.IdentityModel.Tokens;

public class AddKeycloakWebApiAuthenticationTests(KeycloakFixture fixture)
: AuthenticationScenario(fixture)
public class AddKeycloakWebApiAuthenticationTests : AuthenticationScenarioNoKeycloak
{
private const string Endpoint1 = "/endpoints/1";
private static readonly string AppSettings = "appsettings.json";

private static readonly JwtBearerOptions ExpectedAppSettingsJwtBearerOptions =
new()
{
Authority = "http://localhost:8080/realms/Test",
Audience = "test-client",
RequireHttpsMetadata = false,
TokenValidationParameters = new TokenValidationParameters { ValidateAudience = false },
MetadataAddress = "http://localhost:8080/realms/Test/.well-known/openid-configuration",
};

private static readonly string AppSettingsWithOverrides = "appsettings.with-overrides.json";

[Fact]
public async Task AddKeycloakWebApiAuthentication_FromConfiguration_Unauthorized()
{
Expand All @@ -28,6 +40,8 @@ public async Task AddKeycloakWebApiAuthentication_FromConfiguration_Unauthorized
);
});

host.Services.EnsureConfiguredJwtOptions(ExpectedAppSettingsJwtBearerOptions);

await host.Scenario(_ =>
{
_.Get.Url(Endpoint1);
Expand Down Expand Up @@ -60,6 +74,8 @@ public async Task AddKeycloakWebApiAuthentication_FromConfiguration2_Unauthorize
);
});

host.Services.EnsureConfiguredJwtOptions(ExpectedAppSettingsJwtBearerOptions);

await host.Scenario(_ =>
{
_.Get.Url(Endpoint1);
Expand All @@ -85,7 +101,7 @@ public async Task AddKeycloakWebApiAuthentication_FromConfigurationWithOverrides
{
await using var host = await AlbaHost.For<Program>(x =>
{
x.UseConfiguration(AppSettings);
x.UseConfiguration(AppSettingsWithOverrides);
x.ConfigureServices(
(context, services) =>
AddKeycloakWebApiAuthentication_FromConfigurationWithOverrides_Setup(
Expand All @@ -95,6 +111,19 @@ public async Task AddKeycloakWebApiAuthentication_FromConfigurationWithOverrides
);
});

host.Services.EnsureConfiguredJwtOptions(
new JwtBearerOptions
{
Audience = "test-client",
Authority = "http://localhost:8080/realms/DefaultTest",
RequireHttpsMetadata = false,
TokenValidationParameters = new TokenValidationParameters
{
ValidateAudience = true
}
}
);

await host.Scenario(_ =>
{
_.Get.Url(Endpoint1);
Expand All @@ -108,15 +137,49 @@ IConfiguration configuration
)
{
// #region AddKeycloakWebApiAuthentication_FromConfigurationWithOverrides
services.AddKeycloakWebApiAuthentication(configuration);
// #endregion AddKeycloakWebApiAuthentication_FromConfigurationWithOverrides
}

[Fact]
public async Task AddKeycloakWebApiAuthentication_FromConfigurationWithInlineOverrides_Unauthorized()
{
await using var host = await AlbaHost.For<Program>(x =>
{
x.UseConfiguration(AppSettings);
x.ConfigureServices(
(context, services) =>
AddKeycloakWebApiAuthentication_FromConfigurationWithInlineOverrides_Setup(
services,
context.Configuration
)
);
});

host.Services.EnsureConfiguredJwtOptions(ExpectedAppSettingsJwtBearerOptions);

await host.Scenario(_ =>
{
_.Get.Url(Endpoint1);
_.StatusCodeShouldBe(HttpStatusCode.Unauthorized);
});
}

private static void AddKeycloakWebApiAuthentication_FromConfigurationWithInlineOverrides_Setup(
IServiceCollection services,
IConfiguration configuration
)
{
// #region AddKeycloakWebApiAuthentication_FromConfigurationWithInlineOverrides
services.AddKeycloakWebApiAuthentication(
configuration,
(JwtBearerOptions options) =>
(options) =>
{
options.RequireHttpsMetadata = false;
options.Audience = "test-client";
}
);
// #endregion AddKeycloakWebApiAuthentication_FromConfigurationWithOverrides
// #endregion AddKeycloakWebApiAuthentication_FromConfigurationWithInlineOverrides
}

[Fact]
Expand All @@ -134,6 +197,8 @@ public async Task AddKeycloakWebApiAuthentication_FromConfigurationSection_Unaut
);
});

host.Services.EnsureConfiguredJwtOptions(ExpectedAppSettingsJwtBearerOptions);

await host.Scenario(_ =>
{
_.Get.Url(Endpoint1);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
namespace Keycloak.AuthServices.IntegrationTests;
namespace Keycloak.AuthServices.IntegrationTests.ConfigurationTests;

using System.Net;
using Alba;
using Keycloak.AuthServices.Authentication;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;

public class AddKeycloakWebApiTests(KeycloakFixture fixture) : AuthenticationScenario(fixture)
public class AddKeycloakWebApiTests : AuthenticationScenarioNoKeycloak
{
private const string Endpoint1 = "/endpoints/1";
private static readonly string AppSettings = "appsettings.json";
Expand All @@ -20,7 +21,9 @@ public async Task AddKeycloakWebApi_FromConfiguration_Unauthorized()
x.UseConfiguration(AppSettings);
x.ConfigureServices(
(context, services) =>
AddKeycloakWebApi_FromConfiguration_Setup(services, context.Configuration)
{
AddKeycloakWebApi_FromConfiguration_Setup(services, context.Configuration);
}
);
});

Expand Down Expand Up @@ -117,6 +120,9 @@ public async Task AddKeycloakWebApi_FromInline2_Unauthorized()
x.ConfigureServices(AddKeycloakWebApi_FromInline2_Setup);
});

var bearerOptionsM = host.Services.GetService<IOptionsMonitor<JwtBearerOptions>>();
var bearerOptions = bearerOptionsM?.Get(JwtBearerDefaults.AuthenticationScheme);

await host.Scenario(_ =>
{
_.Get.Url(Endpoint1);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
namespace Keycloak.AuthServices.IntegrationTests.ConfigurationTests;

using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;

public static class AssertionsUtils
{
public static void EnsureConfiguredJwtOptions(
this IServiceProvider serviceProvider,
JwtBearerOptions? expected
)
{
var bearerOptionsMonitor = serviceProvider.GetService<IOptionsMonitor<JwtBearerOptions>>();
EnsureMatchingJwtOptions(
bearerOptionsMonitor?.Get(JwtBearerDefaults.AuthenticationScheme),
expected
);
}

public static void EnsureMatchingJwtOptions(
JwtBearerOptions? source,
JwtBearerOptions? expected
)
{
source
.Should()
.BeEquivalentTo(
expected,
cfg =>
cfg.Including(f => f!.Audience)
.Including(f => f!.Authority)
.Including(f => f!.RequireHttpsMetadata)
.Including(f => f!.TokenValidationParameters.ValidateAudience)
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,7 @@ public static IWebHostBuilder UseConfiguration(
this IWebHostBuilder hostBuilder,
string fileName
) =>
hostBuilder.UseConfiguration(
new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile(fileName)
.AddEnvironmentVariables()
.Build()
hostBuilder.ConfigureAppConfiguration(x =>
x.AddJsonFile(Path.Combine(Directory.GetCurrentDirectory(), fileName), optional: false)
);
}
11 changes: 8 additions & 3 deletions tests/Keycloak.AuthServices.IntegrationTests/KeycloakFixture.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,8 @@

public class KeycloakFixture : IAsyncLifetime
{
public KeycloakContainer Keycloak { get; } = new KeycloakBuilder()
.WithImage("quay.io/keycloak/keycloak:24.0.3")
.Build();
public KeycloakContainer Keycloak { get; } =
new KeycloakBuilder().WithImage("quay.io/keycloak/keycloak:24.0.3").Build();

public string BaseAddress => this.Keycloak.GetBaseAddress();
public string ContainerId => $"{this.Keycloak.Id}";
Expand All @@ -24,3 +23,9 @@ public abstract class AuthenticationScenario(KeycloakFixture fixture)
{
public KeycloakContainer Keycloak { get; } = fixture.Keycloak;
}

[CollectionDefinition(nameof(AuthenticationCollectionNoKeycloak))]
public class AuthenticationCollectionNoKeycloak;

[Collection(nameof(AuthenticationCollectionNoKeycloak))]
public abstract class AuthenticationScenarioNoKeycloak;
Loading

0 comments on commit 7aafd05

Please sign in to comment.