Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix Keycloak throwing exception for Secret Key if using public access type #613

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 16 additions & 1 deletion docs/keycloak.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,19 @@ services.AddAuthentication(options => /* Auth configuration */)
});
```

### Production with Public Access Type

```csharp
services.AddAuthentication(options => /* Auth configuration */)
.AddKeycloak(options =>
{
options.AccessType = KeycloakAuthenticationAccessType.Public;
options.ClientId = "my-client-id";
options.Domain = "mydomain.local";
options.Realm = "myrealm";
});
```

### Local Development with Docker

```csharp
Expand All @@ -40,4 +53,6 @@ Only one of either `BaseAddress` or `Domain` is required to be set. If both are

## Optional Settings

_None._
| Property Name | Property Type | Description | Default Value |
| :------------ | :--------------------------------- | :--------------------------------------- | :---------------------------------------------- |
| `AccessType` | `KeycloakAuthenticationAccessType` | The Keycloak client's access token type. | `KeycloakAuthenticationAccessType.Confidential` |
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
// See https://github.com/aspnet-contrib/AspNet.Security.OAuth.Providers
// for more information concerning the license and the contributors participating to this project.

namespace AspNet.Security.OAuth.Keycloak
{
public enum KeycloakAuthenticationAccessType
{
Confidential,
Public,
BearerOnly,
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,11 @@ public KeycloakAuthenticationOptions()
ClaimActions.MapJsonKey(ClaimTypes.Role, "roles");
}

/// <summary>
/// Gets or sets the value for the Keycloak client's access type.
/// </summary>
public KeycloakAuthenticationAccessType AccessType { get; set; }

/// <summary>
/// Gets or sets the base address of the Keycloak server.
/// </summary>
Expand All @@ -51,5 +56,39 @@ public KeycloakAuthenticationOptions()
/// Gets or sets the Keycloak realm to use for authentication.
/// </summary>
public string? Realm { get; set; }

/// <inheritdoc />
public override void Validate()
{
try
{
// HACK We want all of the base validation except for ClientSecret,
// so rather than re-implement it all, catch the exception thrown
// for that being null and only throw if we aren't using public access type.
// This does mean that three checks have to be re-implemented
// because the won't be validated if the ClientSecret validation fails.
base.Validate();
}
catch (ArgumentException ex) when (ex.ParamName == nameof(ClientSecret) && AccessType == KeycloakAuthenticationAccessType.Public)
{
// No client secret is required for a public key.
// See https://github.com/aspnet-contrib/AspNet.Security.OAuth.Providers/issues/610.
}

if (string.IsNullOrEmpty(AuthorizationEndpoint))
{
throw new ArgumentException($"The '{nameof(AuthorizationEndpoint)}' option must be provided.", nameof(AuthorizationEndpoint));
}

if (string.IsNullOrEmpty(TokenEndpoint))
{
throw new ArgumentException($"The '{nameof(TokenEndpoint)}' option must be provided.", nameof(TokenEndpoint));
}

if (!CallbackPath.HasValue)
{
throw new ArgumentException($"The '{nameof(CallbackPath)}' option must be provided.", nameof(CallbackPath));
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
/*
* Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
* See https://github.com/aspnet-contrib/AspNet.Security.OAuth.Providers
* for more information concerning the license and the contributors participating to this project.
*/

using System;
using System.Collections.Generic;
using Xunit;

namespace AspNet.Security.OAuth.Keycloak
{
public static class KeycloakAuthenticationOptionsTests
{
public static IEnumerable<object[]> AccessTypes => new object[][]
{
new object[] { KeycloakAuthenticationAccessType.BearerOnly },
new object[] { KeycloakAuthenticationAccessType.Confidential },
new object[] { KeycloakAuthenticationAccessType.Public },
};

[Theory]
[InlineData(null)]
[InlineData("")]
public static void Validate_Does_Not_Throw_If_ClientSecret_Is_Not_Provided_For_Public_Access_Type(string clientSecret)
{
// Arrange
var options = new KeycloakAuthenticationOptions()
{
AccessType = KeycloakAuthenticationAccessType.Public,
ClientId = "my-client-id",
ClientSecret = clientSecret,
};

// Act (no Assert)
options.Validate();
}

[Theory]
[InlineData(KeycloakAuthenticationAccessType.BearerOnly)]
[InlineData(KeycloakAuthenticationAccessType.Confidential)]
public static void Validate_Throws_If_ClientSecret_Is_Null(KeycloakAuthenticationAccessType accessType)
{
// Arrange
var options = new KeycloakAuthenticationOptions()
{
AccessType = accessType,
ClientId = "my-client-id",
ClientSecret = null,
};

// Act and Assert
Assert.Throws<ArgumentException>("ClientSecret", () => options.Validate());
}

[Theory]
[MemberData(nameof(AccessTypes))]
public static void Validate_Throws_If_AuthorizationEndpoint_Is_Null(KeycloakAuthenticationAccessType accessType)
{
// Arrange
var options = new KeycloakAuthenticationOptions()
{
AccessType = accessType,
AuthorizationEndpoint = null,
ClientId = "my-client-id",
ClientSecret = "my-client-secret",
};

// Act and Assert
Assert.Throws<ArgumentException>("AuthorizationEndpoint", () => options.Validate());
}

[Theory]
[MemberData(nameof(AccessTypes))]
public static void Validate_Throws_If_TokenEndpoint_Is_Null(KeycloakAuthenticationAccessType accessType)
{
// Arrange
var options = new KeycloakAuthenticationOptions()
{
AccessType = accessType,
ClientId = "my-client-id",
ClientSecret = "my-client-secret",
TokenEndpoint = null,
};

// Act and Assert
Assert.Throws<ArgumentException>("TokenEndpoint", () => options.Validate());
}

[Theory]
[MemberData(nameof(AccessTypes))]
public static void Validate_Throws_If_CallbackPath_Is_Null(KeycloakAuthenticationAccessType accessType)
{
// Arrange
var options = new KeycloakAuthenticationOptions()
{
AccessType = accessType,
CallbackPath = null,
ClientId = "my-client-id",
ClientSecret = "my-client-secret",
};

// Act and Assert
Assert.Throws<ArgumentException>("CallbackPath", () => options.Validate());
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -84,5 +84,33 @@ static void ConfigureServices(IServiceCollection services)
// Assert
AssertClaim(claims, claimType, claimValue);
}

[Theory]
[InlineData(ClaimTypes.NameIdentifier, "995c1500-0dca-495e-ba72-2499d370d181")]
[InlineData(ClaimTypes.Email, "john@smith.com")]
[InlineData(ClaimTypes.GivenName, "John")]
[InlineData(ClaimTypes.Role, "admin")]
[InlineData(ClaimTypes.Name, "John Smith")]
public async Task Can_Sign_In_Using_Keycloak_Public_AccessType(string claimType, string claimValue)
{
// Arrange
static void ConfigureServices(IServiceCollection services)
{
services.PostConfigureAll<KeycloakAuthenticationOptions>((options) =>
{
options.AccessType = KeycloakAuthenticationAccessType.Public;
options.ClientSecret = string.Empty;
options.Domain = "keycloak.local";
});
}

using var server = CreateTestServer(ConfigureServices);

// Act
var claims = await AuthenticateUserAsync(server);

// Assert
AssertClaim(claims, claimType, claimValue);
}
}
}