diff --git a/src/Aspire.Hosting.Keycloak/KeycloakRealmResource.cs b/src/Aspire.Hosting.Keycloak/KeycloakRealmResource.cs
new file mode 100644
index 0000000000..6070e77ad7
--- /dev/null
+++ b/src/Aspire.Hosting.Keycloak/KeycloakRealmResource.cs
@@ -0,0 +1,129 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Aspire.Hosting.ApplicationModel;
+
+namespace Aspire.Hosting.Keycloak;
+
+///
+/// Represents a Keycloak Realm resource.
+///
+public sealed class KeycloakRealmResource : Resource, IResourceWithParent, IResourceWithConnectionString
+{
+ private EndpointReference? _parentEndpoint;
+ private EndpointReferenceExpression? _parentUrl;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The name of the realm resource.
+ /// The name of the realm.
+ /// The Keycloak server resource associated with this database.
+ public KeycloakRealmResource(string name, string realmName, KeycloakResource parent) : base(name)
+ {
+ ArgumentException.ThrowIfNullOrWhiteSpace(realmName);
+ ArgumentNullException.ThrowIfNull(parent);
+
+ RealmName = realmName;
+ RealmPath = $"realms/{realmName}";
+ Parent = parent;
+ }
+
+ private EndpointReferenceExpression ParentUrl => _parentUrl ??= ParentEndpoint.Property(EndpointProperty.Url);
+
+ ///
+ /// Gets the parent endpoint reference.
+ ///
+ public EndpointReference ParentEndpoint => _parentEndpoint ??= new(Parent, "http");
+
+ ///
+ public ReferenceExpression ConnectionStringExpression => ReferenceExpression.Create($"{ParentUrl}/{RealmPath}/");
+
+ ///
+ /// Gets the base address of the realm.
+ ///
+ public string RealmPath { get; }
+
+ ///
+ /// Gets the issuer expression for the Keycloak realm.
+ ///
+ public ReferenceExpression IssuerUrlExpression => ReferenceExpression.Create($"{ParentUrl}/{RealmPath}");
+
+ ///
+ /// Gets or sets the metadata address for the Keycloak realm.
+ ///
+ public string MetadataAddress => ".well-known/openid-configuration";
+
+ ///
+ /// Gets the metadata address expression for the Keycloak realm.
+ ///
+ public ReferenceExpression MetadataAddressExpression => ReferenceExpression.Create($"{ConnectionStringExpression}{MetadataAddress}");
+
+ ///
+ /// Gets or sets the 'authorization_endpoint' for the Keycloak realm.
+ ///
+ public string AuthorizationEndpoint => "protocol/openid-connect/auth";
+
+ ///
+ /// Gets the 'authorization_endpoint' expression for the Keycloak realm.
+ ///
+ public ReferenceExpression AuthorizationEndpointExpression => ReferenceExpression.Create($"{ConnectionStringExpression}{AuthorizationEndpoint}");
+
+ ///
+ /// Gets or sets the 'token_endpoint' for the Keycloak realm.
+ ///
+ public string TokenEndpoint => "protocol/openid-connect/token";
+
+ ///
+ /// Gets the 'token_endpoint' expression for the Keycloak realm.
+ ///
+ public ReferenceExpression TokenEndpointExpression => ReferenceExpression.Create($"{ConnectionStringExpression}{TokenEndpoint}");
+
+ ///
+ /// Gets or sets the 'introspection_endpoint' for the Keycloak realm.
+ ///
+ public string IntrospectionEndpoint => "protocol/openid-connect/token/introspect";
+
+ ///
+ /// Gets the 'introspection_endpoint' expression for the Keycloak realm.
+ ///
+ public ReferenceExpression IntrospectionEndpointExpression => ReferenceExpression.Create($"{ConnectionStringExpression}{IntrospectionEndpoint}");
+
+ ///
+ /// Gets or sets 'user_info_endpoint' for the Keycloak realm.
+ ///
+ public string UserInfoEndpoint => "protocol/openid-connect/userinfo";
+
+ ///
+ /// Gets 'user_info_endpoint' expression for the Keycloak realm.
+ ///
+ public ReferenceExpression UserInfoEndpointExpression => ReferenceExpression.Create($"{ConnectionStringExpression}{UserInfoEndpoint}");
+
+ ///
+ /// Gets or sets the 'end_session_endpoint' for the Keycloak realm.
+ ///
+ public string EndSessionEndpoint => "protocol/openid-connect/logout";
+
+ ///
+ /// Gets the 'end_session_endpoint' expression for the Keycloak realm.
+ ///
+ public ReferenceExpression EndSessionEndpointExpression => ReferenceExpression.Create($"{ConnectionStringExpression}{EndSessionEndpoint}");
+
+ ///
+ /// Gets or sets the 'registration_endpoint' for the Keycloak realm.
+ ///
+ public string RegistrationEndpoint => "clients-registrations/openid-connect";
+
+ ///
+ /// Gets the 'registration_endpoint' expression for the Keycloak realm.
+ ///
+ public ReferenceExpression RegistrationEndpointExpression => ReferenceExpression.Create($"{ConnectionStringExpression}{RegistrationEndpoint}");
+
+ ///
+ public KeycloakResource Parent { get; }
+
+ ///
+ /// Gets the name of the realm.
+ ///
+ public string RealmName { get; }
+}
diff --git a/src/Aspire.Hosting.Keycloak/KeycloakResourceBuilderExtensions.cs b/src/Aspire.Hosting.Keycloak/KeycloakResourceBuilderExtensions.cs
index ffd6758a38..dee6360ff6 100644
--- a/src/Aspire.Hosting.Keycloak/KeycloakResourceBuilderExtensions.cs
+++ b/src/Aspire.Hosting.Keycloak/KeycloakResourceBuilderExtensions.cs
@@ -175,4 +175,26 @@ public static IResourceBuilder WithRealmImport(
throw new InvalidOperationException($"The realm import file or directory '{importFullPath}' does not exist.");
}
+
+ ///
+ /// Adds a Keycloak Realm to the application model from a .
+ ///
+ /// The Keycloak server resource builder.
+ /// The name of the realm.
+ /// The name of the realm. If not provided, the resource name will be used.
+ /// A reference to the .
+ public static IResourceBuilder AddRealm(
+ this IResourceBuilder builder,
+ string name,
+ string? realmName = null)
+ {
+ ArgumentNullException.ThrowIfNull(builder);
+
+ // Use the resource name as the realm name if it's not provided
+ realmName ??= name;
+
+ var keycloakRealm = new KeycloakRealmResource(name, realmName, builder.Resource);
+
+ return builder.ApplicationBuilder.AddResource(keycloakRealm);
+ }
}
diff --git a/tests/Aspire.Hosting.Keycloak.Tests/KeycloakPublicApiTests.cs b/tests/Aspire.Hosting.Keycloak.Tests/KeycloakPublicApiTests.cs
index cb3c701faf..4e90e55f65 100644
--- a/tests/Aspire.Hosting.Keycloak.Tests/KeycloakPublicApiTests.cs
+++ b/tests/Aspire.Hosting.Keycloak.Tests/KeycloakPublicApiTests.cs
@@ -34,6 +34,49 @@ public void CtorKeycloakResourceShouldThrowWhenAdminPasswordIsNull()
Assert.Equal(nameof(adminPassword), exception.ParamName);
}
+ [Fact]
+ public void CtorKeycloakRealmResourceShouldThrowWhenNameIsNull()
+ {
+ string name = null!;
+ var realmName = "realm1";
+ var builder = TestDistributedApplicationBuilder.Create();
+ var adminPassword = builder.AddParameter("Password");
+ var parent = new KeycloakResource("keycloak", default(ParameterResource?), adminPassword.Resource);
+
+ var action = () => new KeycloakRealmResource(name, realmName, parent);
+
+ var exception = Assert.Throws(action);
+ Assert.Equal(nameof(name), exception.ParamName);
+ }
+
+ [Fact]
+ public void CtorMongoKeycloakRealmResourceShouldThrowWhenRealmNameIsNull()
+ {
+ var name = "keycloak";
+ string realmName = null!;
+ var builder = TestDistributedApplicationBuilder.Create();
+ var adminPassword = builder.AddParameter("Password");
+ var parent = new KeycloakResource("keycloak", default(ParameterResource?), adminPassword.Resource);
+
+ var action = () => new KeycloakRealmResource(name, realmName, parent);
+
+ var exception = Assert.Throws(action);
+ Assert.Equal(nameof(realmName), exception.ParamName);
+ }
+
+ [Fact]
+ public void CtorMongoKeycloakRealmResourceShouldThrowWhenDatabaseParentIsNull()
+ {
+ var name = "keycloak";
+ var realmName = "realm1";
+ KeycloakResource parent = null!;
+
+ var action = () => new KeycloakRealmResource(name, realmName, parent);
+
+ var exception = Assert.Throws(action);
+ Assert.Equal(nameof(parent), exception.ParamName);
+ }
+
[Fact]
public void AddKeycloakContainerShouldThrowWhenBuilderIsNull()
{
@@ -195,4 +238,29 @@ public void WithRealmImportFileAddsBindMountAnnotation(bool? isReadOnly)
Assert.Equal(ContainerMountType.BindMount, containerAnnotation.Type);
Assert.Equal(isReadOnly ?? false, containerAnnotation.IsReadOnly);
}
+
+ [Fact]
+ public void AddRealmShouldThrowWhenBuilderIsNull()
+ {
+ IResourceBuilder builder = null!;
+ const string name = "realm1";
+
+ var action = () => builder.AddRealm(name);
+
+ var exception = Assert.Throws(action);
+ Assert.Equal(nameof(builder), exception.ParamName);
+ }
+
+ [Fact]
+ public void AddRealmShouldThrowWhenNameIsNull()
+ {
+ var builderResource = TestDistributedApplicationBuilder.Create();
+ var MongoDB = builderResource.AddKeycloak("realm1");
+ string name = null!;
+
+ var action = () => MongoDB.AddRealm(name);
+
+ var exception = Assert.Throws(action);
+ Assert.Equal(nameof(name), exception.ParamName);
+ }
}