diff --git a/Testcontainers.sln b/Testcontainers.sln
index a86fd5cb6..793db4b9c 100644
--- a/Testcontainers.sln
+++ b/Testcontainers.sln
@@ -31,6 +31,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.K3s", "src\T
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.Kafka", "src\Testcontainers.Kafka\Testcontainers.Kafka.csproj", "{E93E40CE-59AA-4561-9B4C-E7B0A686326E}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.Keycloak", "src\Testcontainers.Keycloak\Testcontainers.Keycloak.csproj", "{AA8834A3-82A7-4E83-8E4C-88D37F74056A}"
+EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.LocalStack", "src\Testcontainers.LocalStack\Testcontainers.LocalStack.csproj", "{3792268A-EF08-4569-8118-991E08FD61C4}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.MariaDb", "src\Testcontainers.MariaDb\Testcontainers.MariaDb.csproj", "{4B204EB3-C478-422E-9B6F-62DF3871291A}"
@@ -83,6 +85,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.K3s.Tests",
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.Kafka.Tests", "tests\Testcontainers.Kafka.Tests\Testcontainers.Kafka.Tests.csproj", "{6F2AEE03-629A-4B49-BD5B-25CA3C61FFB7}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.Keycloak.Tests", "tests\Testcontainers.Keycloak.Tests\Testcontainers.Keycloak.Tests.csproj", "{4827D606-89D5-4E00-8341-47A6E95817BA}"
+EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.LocalStack.Tests", "tests\Testcontainers.LocalStack.Tests\Testcontainers.LocalStack.Tests.csproj", "{728CBE16-1D52-4F84-AF01-7229E6013512}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Testcontainers.MariaDb.Tests", "tests\Testcontainers.MariaDb.Tests\Testcontainers.MariaDb.Tests.csproj", "{7F0AE083-9DB8-4BD4-91F7-C199DCC7301D}"
@@ -166,6 +170,10 @@ Global
{E93E40CE-59AA-4561-9B4C-E7B0A686326E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{E93E40CE-59AA-4561-9B4C-E7B0A686326E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{E93E40CE-59AA-4561-9B4C-E7B0A686326E}.Release|Any CPU.Build.0 = Release|Any CPU
+ {AA8834A3-82A7-4E83-8E4C-88D37F74056A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {AA8834A3-82A7-4E83-8E4C-88D37F74056A}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {AA8834A3-82A7-4E83-8E4C-88D37F74056A}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {AA8834A3-82A7-4E83-8E4C-88D37F74056A}.Release|Any CPU.Build.0 = Release|Any CPU
{3792268A-EF08-4569-8118-991E08FD61C4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{3792268A-EF08-4569-8118-991E08FD61C4}.Debug|Any CPU.Build.0 = Debug|Any CPU
{3792268A-EF08-4569-8118-991E08FD61C4}.Release|Any CPU.ActiveCfg = Release|Any CPU
@@ -270,6 +278,10 @@ Global
{6F2AEE03-629A-4B49-BD5B-25CA3C61FFB7}.Debug|Any CPU.Build.0 = Debug|Any CPU
{6F2AEE03-629A-4B49-BD5B-25CA3C61FFB7}.Release|Any CPU.ActiveCfg = Release|Any CPU
{6F2AEE03-629A-4B49-BD5B-25CA3C61FFB7}.Release|Any CPU.Build.0 = Release|Any CPU
+ {4827D606-89D5-4E00-8341-47A6E95817BA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {4827D606-89D5-4E00-8341-47A6E95817BA}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {4827D606-89D5-4E00-8341-47A6E95817BA}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {4827D606-89D5-4E00-8341-47A6E95817BA}.Release|Any CPU.Build.0 = Release|Any CPU
{728CBE16-1D52-4F84-AF01-7229E6013512}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{728CBE16-1D52-4F84-AF01-7229E6013512}.Debug|Any CPU.Build.0 = Debug|Any CPU
{728CBE16-1D52-4F84-AF01-7229E6013512}.Release|Any CPU.ActiveCfg = Release|Any CPU
@@ -357,6 +369,7 @@ Global
{84D707E0-C9FA-4327-85DC-0AFEBEA73572} = {673F23AE-7694-4BB9-ABD4-136D6C13634E}
{111B840F-9DB0-4166-83E6-0580FD418F07} = {673F23AE-7694-4BB9-ABD4-136D6C13634E}
{E93E40CE-59AA-4561-9B4C-E7B0A686326E} = {673F23AE-7694-4BB9-ABD4-136D6C13634E}
+ {AA8834A3-82A7-4E83-8E4C-88D37F74056A} = {673F23AE-7694-4BB9-ABD4-136D6C13634E}
{3792268A-EF08-4569-8118-991E08FD61C4} = {673F23AE-7694-4BB9-ABD4-136D6C13634E}
{4B204EB3-C478-422E-9B6F-62DF3871291A} = {673F23AE-7694-4BB9-ABD4-136D6C13634E}
{1266E1E6-5CEF-4161-8B45-83282455746E} = {673F23AE-7694-4BB9-ABD4-136D6C13634E}
@@ -383,6 +396,7 @@ Global
{64F8E9B9-78FD-4E13-BDDF-0340E2D4E1D0} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF}
{F0F40AE2-70FF-4191-ADDA-26A19E0D1A0F} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF}
{6F2AEE03-629A-4B49-BD5B-25CA3C61FFB7} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF}
+ {4827D606-89D5-4E00-8341-47A6E95817BA} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF}
{728CBE16-1D52-4F84-AF01-7229E6013512} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF}
{7F0AE083-9DB8-4BD4-91F7-C199DCC7301D} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF}
{5DB1F35F-B714-4B62-84BE-16A33084D3E1} = {7164F1FB-7F24-444A-ACD2-2C329C2B3CCF}
diff --git a/src/Testcontainers.Keycloak/.editorconfig b/src/Testcontainers.Keycloak/.editorconfig
new file mode 100644
index 000000000..6f066619d
--- /dev/null
+++ b/src/Testcontainers.Keycloak/.editorconfig
@@ -0,0 +1 @@
+root = true
\ No newline at end of file
diff --git a/src/Testcontainers.Keycloak/KeycloakBuilder.cs b/src/Testcontainers.Keycloak/KeycloakBuilder.cs
new file mode 100644
index 000000000..933cbae0a
--- /dev/null
+++ b/src/Testcontainers.Keycloak/KeycloakBuilder.cs
@@ -0,0 +1,111 @@
+namespace Testcontainers.Keycloak;
+
+///
+[PublicAPI]
+public sealed class KeycloakBuilder : ContainerBuilder
+{
+ public const string KeycloakImage = "quay.io/keycloak/keycloak:21.1";
+
+ public const ushort KeycloakPort = 8080;
+
+ public const string DefaultUsername = "admin";
+
+ public const string DefaultPassword = "admin";
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public KeycloakBuilder()
+ : this(new KeycloakConfiguration())
+ {
+ DockerResourceConfiguration = Init().DockerResourceConfiguration;
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The Docker resource configuration.
+ private KeycloakBuilder(KeycloakConfiguration resourceConfiguration)
+ : base(resourceConfiguration)
+ {
+ DockerResourceConfiguration = resourceConfiguration;
+ }
+
+ ///
+ protected override KeycloakConfiguration DockerResourceConfiguration { get; }
+
+ ///
+ /// Sets the Keycloak admin username.
+ ///
+ /// The Keycloak admin username.
+ /// A configured instance of .
+ public KeycloakBuilder WithUsername(string username)
+ {
+ return Merge(DockerResourceConfiguration, new KeycloakConfiguration(username: username))
+ .WithEnvironment("KEYCLOAK_ADMIN", username);
+ }
+
+ ///
+ /// Sets the Keycloak admin password.
+ ///
+ /// The Keycloak admin password.
+ /// A configured instance of .
+ public KeycloakBuilder WithPassword(string password)
+ {
+ return Merge(DockerResourceConfiguration, new KeycloakConfiguration(password: password))
+ .WithEnvironment("KEYCLOAK_ADMIN_PASSWORD", password);
+ }
+
+ ///
+ public override KeycloakContainer Build()
+ {
+ Validate();
+ return new KeycloakContainer(DockerResourceConfiguration, TestcontainersSettings.Logger);
+ }
+
+ ///
+ protected override KeycloakBuilder Init()
+ {
+ return base.Init()
+ .WithImage(KeycloakImage)
+ .WithCommand("start-dev")
+ .WithPortBinding(KeycloakPort, true)
+ .WithUsername(DefaultUsername)
+ .WithPassword(DefaultPassword)
+ .WithEnvironment("KC_HEALTH_ENABLED", "true")
+ .WithWaitStrategy(Wait.ForUnixContainer().UntilHttpRequestIsSucceeded(request =>
+ request.ForPath("/health/ready").ForPort(KeycloakPort)));
+ }
+
+ ///
+ protected override void Validate()
+ {
+ base.Validate();
+
+ _ = Guard.Argument(DockerResourceConfiguration.Username, nameof(DockerResourceConfiguration.Username))
+ .NotNull()
+ .NotEmpty();
+
+ _ = Guard.Argument(DockerResourceConfiguration.Password, nameof(DockerResourceConfiguration.Password))
+ .NotNull()
+ .NotEmpty();
+ }
+
+ ///
+ protected override KeycloakBuilder Clone(IResourceConfiguration resourceConfiguration)
+ {
+ return Merge(DockerResourceConfiguration, new KeycloakConfiguration(resourceConfiguration));
+ }
+
+ ///
+ protected override KeycloakBuilder Clone(IContainerConfiguration resourceConfiguration)
+ {
+ return Merge(DockerResourceConfiguration, new KeycloakConfiguration(resourceConfiguration));
+ }
+
+ ///
+ protected override KeycloakBuilder Merge(KeycloakConfiguration oldValue, KeycloakConfiguration newValue)
+ {
+ return new KeycloakBuilder(new KeycloakConfiguration(oldValue, newValue));
+ }
+}
\ No newline at end of file
diff --git a/src/Testcontainers.Keycloak/KeycloakConfiguration.cs b/src/Testcontainers.Keycloak/KeycloakConfiguration.cs
new file mode 100644
index 000000000..b1b2937e8
--- /dev/null
+++ b/src/Testcontainers.Keycloak/KeycloakConfiguration.cs
@@ -0,0 +1,69 @@
+namespace Testcontainers.Keycloak;
+
+///
+[PublicAPI]
+public sealed class KeycloakConfiguration : ContainerConfiguration
+{
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The admin username.
+ /// The admin password.
+ public KeycloakConfiguration(string username = null, string password = null)
+ {
+ Username = username;
+ Password = password;
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The Docker resource configuration.
+ public KeycloakConfiguration(IResourceConfiguration resourceConfiguration)
+ : base(resourceConfiguration)
+ {
+ // Passes the configuration upwards to the base implementations to create an updated immutable copy.
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The Docker resource configuration.
+ public KeycloakConfiguration(IContainerConfiguration resourceConfiguration)
+ : base(resourceConfiguration)
+ {
+ // Passes the configuration upwards to the base implementations to create an updated immutable copy.
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The Docker resource configuration.
+ public KeycloakConfiguration(KeycloakConfiguration resourceConfiguration)
+ : this(new KeycloakConfiguration(), resourceConfiguration)
+ {
+ // Passes the configuration upwards to the base implementations to create an updated immutable copy.
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The old Docker resource configuration.
+ /// The new Docker resource configuration.
+ public KeycloakConfiguration(KeycloakConfiguration oldValue, KeycloakConfiguration newValue)
+ : base(oldValue, newValue)
+ {
+ Username = BuildConfiguration.Combine(oldValue.Username, newValue.Username);
+ Password = BuildConfiguration.Combine(oldValue.Password, newValue.Password);
+ }
+
+ ///
+ /// Gets the admin username.
+ ///
+ public string Username { get; } = null!;
+
+ ///
+ /// Gets the admin password.
+ ///
+ public string Password { get; } = null!;
+}
\ No newline at end of file
diff --git a/src/Testcontainers.Keycloak/KeycloakContainer.cs b/src/Testcontainers.Keycloak/KeycloakContainer.cs
new file mode 100644
index 000000000..b2a9b93fb
--- /dev/null
+++ b/src/Testcontainers.Keycloak/KeycloakContainer.cs
@@ -0,0 +1,28 @@
+namespace Testcontainers.Keycloak;
+
+///
+[PublicAPI]
+public sealed class KeycloakContainer : DockerContainer
+{
+ private readonly KeycloakConfiguration _configuration;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The container configuration.
+ /// The logger.
+ public KeycloakContainer(KeycloakConfiguration configuration, ILogger logger)
+ : base(configuration, logger)
+ {
+ _configuration = configuration;
+ }
+
+ ///
+ /// Gets the Keycloak base address.
+ ///
+ /// The Keycloak base address.
+ public string GetBaseAddress()
+ {
+ return new UriBuilder(Uri.UriSchemeHttp, Hostname, GetMappedPublicPort(KeycloakBuilder.KeycloakPort)).ToString();
+ }
+}
\ No newline at end of file
diff --git a/src/Testcontainers.Keycloak/Testcontainers.Keycloak.csproj b/src/Testcontainers.Keycloak/Testcontainers.Keycloak.csproj
new file mode 100644
index 000000000..4c05d521f
--- /dev/null
+++ b/src/Testcontainers.Keycloak/Testcontainers.Keycloak.csproj
@@ -0,0 +1,13 @@
+
+
+ netstandard2.0;netstandard2.1
+ latest
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/Testcontainers.Keycloak/Usings.cs b/src/Testcontainers.Keycloak/Usings.cs
new file mode 100644
index 000000000..4ec1e3a96
--- /dev/null
+++ b/src/Testcontainers.Keycloak/Usings.cs
@@ -0,0 +1,9 @@
+global using System;
+global using System.Threading.Tasks;
+global using Docker.DotNet.Models;
+global using DotNet.Testcontainers;
+global using DotNet.Testcontainers.Builders;
+global using DotNet.Testcontainers.Configurations;
+global using DotNet.Testcontainers.Containers;
+global using JetBrains.Annotations;
+global using Microsoft.Extensions.Logging;
\ No newline at end of file
diff --git a/tests/Testcontainers.Keycloak.Tests/.editorconfig b/tests/Testcontainers.Keycloak.Tests/.editorconfig
new file mode 100644
index 000000000..6f066619d
--- /dev/null
+++ b/tests/Testcontainers.Keycloak.Tests/.editorconfig
@@ -0,0 +1 @@
+root = true
\ No newline at end of file
diff --git a/tests/Testcontainers.Keycloak.Tests/KeycloakContainerTest.cs b/tests/Testcontainers.Keycloak.Tests/KeycloakContainerTest.cs
new file mode 100644
index 000000000..a194181ed
--- /dev/null
+++ b/tests/Testcontainers.Keycloak.Tests/KeycloakContainerTest.cs
@@ -0,0 +1,45 @@
+namespace Testcontainers.Keycloak.Tests;
+
+public sealed class KeycloakContainerTest : IAsyncLifetime
+{
+ private readonly KeycloakContainer _keycloakContainer = new KeycloakBuilder().Build();
+
+ public Task InitializeAsync()
+ {
+ return _keycloakContainer.StartAsync();
+ }
+
+ public Task DisposeAsync()
+ {
+ return _keycloakContainer.DisposeAsync().AsTask();
+ }
+
+ [Fact]
+ public async Task GetOpenIdEndpointReturnsHttpStatusCodeOk()
+ {
+ // Given
+ using var httpClient = new HttpClient();
+ httpClient.BaseAddress = new Uri(_keycloakContainer.GetBaseAddress());
+
+ // When
+ using var response = await httpClient.GetAsync("/realms/master/.well-known/openid-configuration")
+ .ConfigureAwait(false);
+
+ // Then
+ Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+ }
+
+ [Fact]
+ public async Task MasterRealmIsEnabled()
+ {
+ // Given
+ var keycloakClient = new KeycloakClient(_keycloakContainer.GetBaseAddress(), KeycloakBuilder.DefaultUsername, KeycloakBuilder.DefaultPassword);
+
+ // When
+ var masterRealm = await keycloakClient.GetRealmAsync("master")
+ .ConfigureAwait(false);
+
+ // Then
+ Assert.True(masterRealm.Enabled);
+ }
+}
\ No newline at end of file
diff --git a/tests/Testcontainers.Keycloak.Tests/Testcontainers.Keycloak.Tests.csproj b/tests/Testcontainers.Keycloak.Tests/Testcontainers.Keycloak.Tests.csproj
new file mode 100644
index 000000000..5c5c551e8
--- /dev/null
+++ b/tests/Testcontainers.Keycloak.Tests/Testcontainers.Keycloak.Tests.csproj
@@ -0,0 +1,18 @@
+
+
+ net6.0
+ false
+ false
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/tests/Testcontainers.Keycloak.Tests/Usings.cs b/tests/Testcontainers.Keycloak.Tests/Usings.cs
new file mode 100644
index 000000000..66a86e46d
--- /dev/null
+++ b/tests/Testcontainers.Keycloak.Tests/Usings.cs
@@ -0,0 +1,6 @@
+global using System;
+global using System.Net;
+global using System.Net.Http;
+global using System.Threading.Tasks;
+global using Keycloak.Net;
+global using Xunit;
\ No newline at end of file