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

Add KeycloakRealmResource and AddRealm method #7120

Open
wants to merge 2 commits into
base: main
Choose a base branch
from

Conversation

paulomorgado
Copy link
Contributor

@paulomorgado paulomorgado commented Jan 15, 2025

Description

This PR introduces a new AddRealm method in the KeycloakResourceBuilderExtensions class, enabling the addition of Keycloak Realm resources to the application model. The method constructs a KeycloakRealmResource using an IResourceBuilder<KeycloakResource>, a realm name, and an optional realm name parameter.

Additionally, a new KeycloakRealmResource class is defined, which includes properties for various Keycloak endpoints and their expressions, along with a constructor and detailed XML documentation.

Changes to PublicAPI.Unshipped.txt reflect the inclusion of the new method and class in the public API.

Fixes #5092

Checklist

  • Is this feature complete?
    • Yes. Ready to ship.
    • No. Follow-up changes expected.
  • Are you including unit tests for the changes and scenario tests if relevant?
    • Yes
    • No
  • Did you add public API?
    • Yes
      • If yes, did you have an API Review for it?
        • Yes
        • No
      • Did you add <remarks /> and <code /> elements on your triple slash comments?
        • Yes
        • No
    • No
  • Does the change make any security assumptions or guarantees?
    • Yes
      • If yes, have you done a threat model and had a security review?
        • Yes
        • No
    • No
  • Does the change require an update in our Aspire docs?

@dotnet-policy-service dotnet-policy-service bot added the community-contribution Indicates that the PR has been added by a community member label Jan 15, 2025
@davidfowl
Copy link
Member

No sample or tests?

@paulomorgado
Copy link
Contributor Author

No sample or tests?

Added tests. What did you have in mind for examples?

@paulomorgado paulomorgado marked this pull request as ready for review January 18, 2025 19:48
@davidfowl
Copy link
Member

We have a keycloak playground project. Does it improve because of this new feature?

@paulomorgado
Copy link
Contributor Author

We have a keycloak playground project. Does it improve because of this new feature?

No. The playground project is about putting applications in Keycloak, This is about putting Keycloak in applications.

I'll add an example of how I'm using this next week, you'd like me to. But it requires nothing specific from Keycloak on the application side.

Maybe this could be a generic component for using service discovery with identity.

This commit introduces a new `AddRealm` method in the
`KeycloakResourceBuilderExtensions` class, enabling the
addition of Keycloak Realm resources to the application model.
The method constructs a `KeycloakRealmResource` using an
`IResourceBuilder<KeycloakResource>`, a realm name, and an
optional realm name parameter.

Additionally, a new `KeycloakRealmResource` class is defined,
which includes properties for various Keycloak endpoints and
their expressions, along with a constructor and detailed XML
documentation.

Changes to `PublicAPI.Unshipped.txt` reflect the inclusion
of the new method and class in the public API.
The `KeycloakRealmResource` class has been refactored to include null checks in its constructor for parameters `name`, `realmName`, and `parent`. A private field `_parentEndpoint` and a property `ParentEndpoint` have been added. The `Parent` and `RealmName` properties now rely on the constructor for initialization.

New unit tests in `KeycloakPublicApiTests.cs` ensure that the constructor throws an `ArgumentNullException` for null parameters. Additional tests validate that the `AddRealm` method correctly handles null values for the builder and realm name, improving input validation across the API.
@paulomorgado
Copy link
Contributor Author

paulomorgado commented Jan 21, 2025

@davidfowl,

This is how I've been using this resource:

var idp = builder.AddKeycloak(
    name: "keycloak",
    adminUsername: builder.AddParameter("KeycloakAdminUsername").ExcludeFromManifest(),
    adminPassword: builder.AddParameter("KeycloakAdminPassword").ExcludeFromManifest())
    .WithExternalHttpEndpoints()
    ...
    .AddRealm("idp")
    ;

public static IResourceBuilder<TResource> WithServerAccessTokenHandler<TResource>(
    this IResourceBuilder<TResource> builder,
    IResourceBuilder<KeycloakRealmResource> idp)
    where TResource : IResourceWithEnvironment, IResourceWithWaitSupport
{
    return builder
        .WaitFor(idp)
        .WithEnvironment(ctx =>
        {
            var idpResource = idp.Resource;
            ctx.EnvironmentVariables["JwtBearerOptions__Authority"] = idpResource.ConnectionStringExpression;
            ctx.EnvironmentVariables["JwtBearerOptions__MetadataAddress"] = idpResource.MetadataAddressExpression;
            ctx.EnvironmentVariables["JwtBearerOptions__Configuration__Issuer"] = idpResource.IssuerExpression;
            ctx.EnvironmentVariables["JwtBearerOptions__Configuration__AuthorizationEndpoint"] = idpResource.AuthorizationEndpointExpression;
            ctx.EnvironmentVariables["JwtBearerOptions__Configuration__TokenEndpoint"] = idpResource.TokenEndpointExpression;
            ctx.EnvironmentVariables["JwtBearerOptions__Configuration__UserInfoEndpoint"] = idpResource.UserInfoEndpointExpression;
            ctx.EnvironmentVariables["JwtBearerOptions__RequireHttpsMetadata"] = "false";
            ctx.EnvironmentVariables["JwtBearerOptions__TokenValidationParameters__RequireSignedTokens"] = "false";
            ctx.EnvironmentVariables["JwtBearerOptions__TokenValidationParameters__ValidateAudience"] = "false";
            ctx.EnvironmentVariables["JwtBearerOptions__TokenValidationParameters__ValidateActor"] = "false";
            ctx.EnvironmentVariables["JwtBearerOptions__TokenValidationParameters__ValidateIssuer"] = "false";
            ctx.EnvironmentVariables["JwtBearerOptions__TokenValidationParameters__ValidateIssuerSigningKey"] = "false";
            ctx.EnvironmentVariables["JwtBearerOptions__TokenValidationParameters__ValidateLifetime"] = "false";
        })
        ;
}

public static IResourceBuilder<TResource> WithClientAccessTokenHandler<TResource>(
    this IResourceBuilder<TResource> builder,
    IResourceBuilder<KeycloakRealmResource> idp,
    IResourceBuilder<ParameterResource> touchpointMediaProducerClientId,
    IResourceBuilder<ParameterResource> touchpointMediaProducerClientSecret,
    IResourceBuilder<ParameterResource> touchpointMediaProducerUsername,
    IResourceBuilder<ParameterResource> touchpointMediaProducerPassword)
    where TResource : IResourceWithEnvironment, IResourceWithWaitSupport
{
    return builder
        .WaitFor(idp)
        .WithEnvironment("ClientCredentialsClient__ClientId", touchpointMediaProducerClientId)
        .WithEnvironment("ClientCredentialsClient__ClientSecret", touchpointMediaProducerClientSecret)
        .WithEnvironment("ClientCredentialsClient__Username", touchpointMediaProducerUsername)
        .WithEnvironment("ClientCredentialsClient__Password", touchpointMediaProducerPassword)
        .WithEnvironment("ClientCredentialsClient__TokenEndpoint", idp.Resource.TokenEndpointExpression)
        ;
}
``

On the server, I have:

```csharp
public static void AddAuthentication(this IServiceCollection services, IConfiguration configuration)
{
    services.AddDistributedMemoryCache();

    services
        .AddOptions<AuthenticationOptions>()
        .BindConfiguration("AuthenticationOptions")
        .ValidateOnStart();

    services.ConfigureOptions<JwtBearerBackchannelPostConfigureOptions>();

    services
        .AddOptions<JwtBearerOptions>("Bearer")
        .BindConfiguration("JwtBearerOptions")
        .ValidateOnStart();

    services
        .AddAuthentication()
        .AddJwtBearer();

    services.AddAuthorization(o => UserClaimsSetter.SetUserClaimsPolicy(o));
}

private sealed class JwtBearerBackchannelPostConfigureOptions : IPostConfigureOptions<JwtBearerOptions>
{
    private readonly IHttpClientFactory _httpClientFactory;

    public JwtBearerBackchannelPostConfigureOptions(IHttpClientFactory httpClientFactory)
    {
        _httpClientFactory = httpClientFactory;
    }
    public void PostConfigure(string? name, JwtBearerOptions options)
    {
        options.Backchannel = string.IsNullOrEmpty(name) ? _httpClientFactory.CreateClient() : _httpClientFactory.CreateClient(name);
    }
}

On the client side, I have components to load IHttpClientFactory definitions and Duende.AccessTokenManagement definitions.

Nothing on theses services is directly dependent on the IDP being used on Aspire, that might or might not be the one used in production.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
community-contribution Indicates that the PR has been added by a community member
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Create KeycloakRealmResource to represent the address of a realm
2 participants