Test support Testcontainers IDP
Overview
Contains support for spinning up the ASOL Identity Provider (IDP) as a Docker testcontainer. The package wraps the standard Testcontainers ContainerBuilder pattern with a dedicated fluent IdpBuilder.
The IDP container can automatically seed MongoDB data before the container starts, so OIDC applications and other initial data are available on first request.
This functionality is not yet complete: currently only a service account can be authenticated. Additional authentication scenarios will be supported in the future.
Install
- ASOL.Core.QA.TestSupport.Testcontainers.Idp
IdpBuilder
IdpBuilder is the entry point for creating an IdpContainer instance. It extends the Testcontainers ContainerBuilder and exposes a fluent API for all IDP configuration concerns.
Construction
// Construct with a full image string
var builder = new IdpBuilder("913524905811.dkr.ecr.eu-central-1.amazonaws.com/ava/asol.identityprovider.api:1.20250915.1");
// Or using the well-known constants
var image = $"{IdpBuilder.DefaultRegistry}/{IdpBuilder.DefaultRepository}:{version}";
var builder = new IdpBuilder(image);
| Constant | Value |
|---|---|
IdpBuilder.DefaultRegistry | 913524905811.dkr.ecr.eu-central-1.amazonaws.com |
IdpBuilder.DefaultRepository | ava/asol.identityprovider.api |
Default configuration (Development environment)
When using IdpBuilder, the following defaults are applied automatically. They can be overridden by calling the corresponding WithXxx methods:
ASPNETCORE_ENVIRONMENT=DevelopmentASPNETCORE_URLS=http://+:80;https://+:443so the container listens on standard ports- http: 80
- https: 443
- Network alias:
idp - Self-signed TLS certificate bundled with the package (
/certs/idp.pfx, passwordpassword) - MongoDB:
DatabaseName=ASP-Master,CollectionNamePrefix=IDP.,HealthCheckMode=SkipAsHealthy - RabbitMQ:
VirtualHost=/,ClientName=ASOL.IdentityProvider, SSL disabled,HealthCheckMode=SkipAsHealthy - Authorization service:
ApplicationCode=ASOLEU-IDP-AP-, strict mode =Warning,HealthCheckMode=SkipAsHealthy - Reverse proxy: disabled
Behavior change: no OIDC application is seeded by default any more. Earlier versions implicitly seeded a
plaza-servicesclient for every consumer. CallWithOIDCApplicationSeed(clientId, clientSecret)with your own credentials instead — see Data Seeding.
Validation:
IdpBuilder.Build()validates that MongoDB connection string, database name, RabbitMQ host name, virtual host, and client name are all set. In non-Developmentenvironments additional fields are also validated.
Fluent methods
WithMongoDb
Configures the MongoDB connection for the IDP. Use the overload that accepts a MongoDbContainer (from IdpBuilderExtensions) for automatic container wiring.
// Direct options
builder.WithMongoDb(new MongoDbConnectionOptions(
ConnectionString: "mongodb://admin:admin@database:27017",
DatabaseName: "ASP-Master",
CollectionNamePrefix: "IDP.",
HealthCheckMode: HealthCheckMode.SkipAsHealthy));
// Via IdpBuilderExtensions shortcut (preferred)
builder.WithMongoDb(mongoDbContainer);
See MongoDbConnectionOptions for all properties.
WithRabbitMq / AddRabbitMq
Configures the RabbitMQ connection for the IDP. Use the AddRabbitMq extension (from IdpBuilderExtensions) for automatic container wiring.
// Direct options
builder.WithRabbitMq(new RabbitMqConnectionOptions(
HostName: "eventbus",
Port: 5672,
UserName: "guest",
Password: "guest",
VirtualHost: "/",
ClientName: "ASOL.IdentityProvider",
Ssl: new RabbitMqSslConnectionOptions(Enabled: false)));
// Via IdpBuilderExtensions shortcut (preferred)
builder.AddRabbitMq(rabbitMqContainer);
See RabbitMqConnectionOptions for all properties.
WithAuthorizationService
builder.WithAuthorizationService(new AuthorizationServiceOptions(
BaseUrl: "https://authz-service/",
ApplicationCode: "ASOLEU-IDP-AP-",
ServiceRoleStrictMode: ServiceAuthorizationStrictMode.Warning,
HealthCheckMode: HealthCheckMode.SkipAsHealthy));
See AuthorizationServiceOptions for all properties.
WithReverseProxy
// Disable reverse proxy (default in Development)
builder.WithReverseProxy(new ReverseProxyOptions(Enabled: false));
// Enable with base path trimming
builder.WithReverseProxy(new ReverseProxyOptions(
Enabled: true,
BasePath: "/idp",
UriMode: ReverseProxyUriMode.Trimmed,
ForceHttpsScheme: true));
See ReverseProxyOptions for all properties.
WithIdentityManager
builder.WithIdentityManager(new IdentityManagerClientOptions(
BaseUrl: "https://identity-manager/"));
WithGoogleReCaptcha
builder.WithGoogleReCaptcha(new GoogleReCaptchaOptions(
SiteKey: "my-site-key",
SecretKey: "my-secret-key",
SuppressCaptchaValidatorUrl: "https://captcha-bypass/"));
See GoogleReCaptchaOptions for all properties.
WithTelemetry
builder.WithTelemetry(new TelemetryOptions(
Endpoint: "https://otel-collector:4317/",
ApiVersion: "v1",
InstrumentationKey: "00000000-0000-0000-0000-000000000000",
ServiceName: "ASOL.IdentityProvider",
LogDirectory: "/logs"));
WithMicrosoftExternalProvider
builder.WithMicrosoftExternalProvider(new MicrosoftExternalProviderOptions(
Authority: "https://login.microsoftonline.com/tenant-id/v2.0",
ClientId: "aad-client-id",
ClientSecret: "aad-client-secret",
DisplayName: "Microsoft",
CallbackPath: "/signin-microsoft"));
WithMessageProvider
builder.WithMessageProvider(new MessageProviderClientOptions(
BaseUrl: "https://message-provider/"));
WithSubjectManager
builder.WithSubjectManager(new SubjectManagerClientOptions(
BaseUrl: "https://subject-manager/"));
WithDistributedCache
Configures the IDP distributed cache storage. The ConnectionString value is passed to the shared RedisConnection configuration section used by Redis-backed Core packages.
The example uses ASOL.Core.QA.TestSupport.Testcontainers.Idp.Options.DistributedCacheOptions, not ASOL.Core.Caching.Distributed.Options.DistributedCacheOptions.
The helper maps ConnectionString to RedisConnection__ConnectionString and keeps Storage in DistributedCache__Storage.
builder.WithDistributedCache(new DistributedCacheOptions(
ConnectionString: "redis:6379",
Storage: "RemoteCache"));
WithCertificateAuthority
builder.WithCertificateAuthority(new CertificateAuthorityOptions(
EncryptionKey: "my-encryption-key"));
WithOidc
builder.WithOidc(new OidcOptions(
CertificatePath: "/certs/oidc.pfx",
DataProtectionFilePersistencePath: "/data-protection-keys",
DataProtectionApplicationCode: "ASOLEU-IDP-AP-",
SigningCertificatePassword: "password",
EncryptionCertificatePassword: "password",
RequireConfirmedAccount: false));
See OidcOptions for all properties.
WithSsoAuth
builder.WithSsoAuth(new SsoAuthOptions(
Authority: "https://idp/",
ClientId: "plaza-core-test",
ClientSecret: "secret",
RequireHttpsMetadata: false,
Audience: "apiim"));
IdpBuilderExtensions
Convenience shortcuts for wiring up MongoDB and RabbitMQ testcontainers directly to an IdpBuilder, using the IDP's default section names. The containers do not need to be started before calling these methods.
WithMongoDb
Wires MongoDbContainer to the IDP using section name "MasterConnectionOptions".
builder.WithMongoDb(mongoDbContainer);
// With optional overrides
builder.WithMongoDb(mongoDbContainer, new PartialMongoDbConnectionOptions
{
CollectionNamePrefix = "IDP."
});
AddRabbitMq
Wires RabbitMqContainer to the IDP using the default messaging section names ("Messaging" / "RabbitMq").
builder.AddRabbitMq(rabbitMqContainer);
// With optional overrides
builder.AddRabbitMq(rabbitMqContainer, new PartialRabbitMqConnectionOptions
{
ClientName = "ASOL.IdentityProvider"
});
Data Seeding
IdpBuilder can seed MongoDB collections before the container starts. Seeding is performed using a temporary mongosh container that runs against the configured MongoDB instance.
WithDataSeed
Adds a generic seed for any MongoDB collection.
builder.WithDataSeed(new DataSeed(
CollectionName: "MyCollection",
Data: new { Field1 = "value1", Field2 = 42 }));
| Parameter | Description |
|---|---|
CollectionName | The MongoDB collection to seed into. |
Data | The object to insert (serialized to JSON). |
WithOIDCApplicationSeed (client credentials)
Seeds the opinionated test OIDC application for the supplied client credentials. This is the replacement for the OIDC application that earlier versions seeded implicitly.
builder.WithOIDCApplicationSeed("plaza-core-test", IdpBuilder.HashClientSecret("Password123*"));
| Parameter | Description |
|---|---|
clientId | The OIDC client identifier to seed. Also used as the display name. |
clientSecret | The client secret value stored in the seed. The IDP expects an already hashed secret; a plaintext value seeds a client that can never authenticate. |
The seed uses ApplicationType=web, ClientType=confidential, ConsentType=implicit, GrantAccessToAllTenants=true, Released=true, and a broad permission set intended for service-to-service test flows (gt:client_credentials, ept:token, rst:id_token token, gt:login_behalf_of, and the scp:* API scopes).
Use IdpBuilder.CreateOIDCApplicationSeed(clientId, clientSecret) to get the same seed as a record, adjust individual values with a with expression, and pass the result to the overload below.
Both overloads append. Each seed is inserted with insertOne, never upserted, so seeding the same client_id twice creates a second IDP client instead of replacing the first one. Nothing detects it — seeding runs against an empty database before the IDP starts, so no unique index on client_id exists yet.
HashClientSecret
Hashes a plaintext client secret into the format the IDP stores and verifies.
var plaintextClientSecret = "Password123*";
var hashedClientSecret = IdpBuilder.HashClientSecret(plaintextClientSecret);
Seed hashedClientSecret and send plaintextClientSecret as the client_secret parameter of the token request.
WithOIDCApplicationSeed (full seed)
Seeds an arbitrary OIDC application into the OIDCApplications collection.
builder.WithOIDCApplicationSeed(new OIDCApplicationSeed(
ApplicationType: "web",
ClientId: "my-client",
ClientSecret: "hashed-secret",
ClientType: "confidential",
ConsentType: "implicit",
DisplayName: "My Application",
DisplayNames: ImmutableDictionary<string, string>.Empty,
Permissions: ["gt:client_credentials", "ept:token", "rst:id_token token"],
PostLogoutRedirectUris: [],
Properties: new object(),
RedirectUris: [],
Requirements: [],
Settings: ImmutableDictionary<string, string>.Empty,
GrantAccessToAllTenants: true,
Released: true,
Deleted: false,
CreatedBy: "test_system",
IntegrationToken: null,
IntegrationTokenSetupCompleted: false,
Description: "My test OIDC application",
IdentityManagementLevel: 1));
The _id and CreatedOn fields are generated automatically by the seeder using MongoDB template tokens.
Examples
Minimal – HTTP / Swagger
public class IdpContainer_Tests : IAsyncLifetime
{
private readonly MongoDbContainer mongoDbContainer;
private readonly RabbitMqContainer rabbitMqContainer;
private readonly RedisContainer redisContainer;
private readonly INetwork network;
public IdpContainer_Tests()
{
network = new NetworkBuilder().Build();
mongoDbContainer = new MongoDbBuilder("mongo:6.0")
.WithNetwork(network)
.WithUsername("admin")
.WithPassword("admin")
.WithNetworkAliases("database")
.Build();
rabbitMqContainer = new RabbitMqBuilder("rabbitmq:3.11")
.WithNetwork(network)
.WithNetworkAliases("eventbus")
.WithUsername("guest")
.WithPassword("guest")
.Build();
redisContainer = new RedisBuilder("redis:8.4")
.WithNetwork(network)
.WithNetworkAliases("redis")
.Build();
}
public ValueTask InitializeAsync() => ValueTask.CompletedTask;
public async ValueTask DisposeAsync()
{
GC.SuppressFinalize(this);
await Task.WhenAll(
mongoDbContainer.DisposeAsync().AsTask(),
rabbitMqContainer.DisposeAsync().AsTask(),
redisContainer.DisposeAsync().AsTask());
}
[Fact]
public async Task Get_Swagger_Success()
{
await using var idpContainer = new IdpBuilder(IdpImageName)
.WithNetwork(network)
.WithMongoDb(mongoDbContainer)
.AddRabbitMq(rabbitMqContainer)
.WithPortBinding(80, assignRandomHostPort: true)
.WithWaitStrategy(Wait.ForUnixContainer().UntilInternalTcpPortIsAvailable(80))
.Build();
await idpContainer.StartAsync(TestContext.Current.CancellationToken);
var handler = new HttpClientHandler
{
ServerCertificateCustomValidationCallback =
HttpClientHandler.DangerousAcceptAnyServerCertificateValidator
};
var port = idpContainer.GetMappedPublicPort(80);
using var httpClient = new HttpClient(handler);
var response = await httpClient.GetAsync(
$"http://localhost:{port}/swagger/index.html",
TestContext.Current.CancellationToken);
Assert.True(response.IsSuccessStatusCode);
}
}
OAuth 2.0 Client Credentials Token
The OIDC client must be seeded explicitly. The seed stores the hash; the token request sends the plaintext.
private const string PlaintextClientSecret = "Password123*";
[Fact]
public async Task Get_ServiceAccount_AccessToken_Success()
{
await using var idpContainer = new IdpBuilder(IdpImageName)
.WithNetwork(network)
.WithMongoDb(mongoDbContainer)
.AddRabbitMq(rabbitMqContainer)
.WithOIDCApplicationSeed("plaza-core-test", IdpBuilder.HashClientSecret(PlaintextClientSecret))
.WithPortBinding(443, assignRandomHostPort: true)
.WithWaitStrategy(Wait.ForUnixContainer().UntilInternalTcpPortIsAvailable(443))
.Build();
await idpContainer.StartAsync(TestContext.Current.CancellationToken);
var handler = new HttpClientHandler
{
ServerCertificateCustomValidationCallback =
HttpClientHandler.DangerousAcceptAnyServerCertificateValidator
};
var tokenRequest = new FormUrlEncodedContent(
[
new KeyValuePair<string, string>("grant_type", "client_credentials"),
new KeyValuePair<string, string>("client_id", "plaza-core-test"),
new KeyValuePair<string, string>("client_secret", PlaintextClientSecret),
]);
var port = idpContainer.GetMappedPublicPort(443);
using var httpClient = new HttpClient(handler);
var tokenResponse = await httpClient.PostAsync(
$"https://localhost:{port}/connect/token",
tokenRequest,
TestContext.Current.CancellationToken);
var tokenContent = await tokenResponse.Content.ReadAsStringAsync(
TestContext.Current.CancellationToken);
Assert.True(tokenResponse.IsSuccessStatusCode, tokenContent);
using var tokenJson = JsonDocument.Parse(tokenContent);
Assert.True(tokenJson.RootElement.TryGetProperty("access_token", out _));
}