[codex] Remove production-usable default admin credentials (#7500)

* Remove default admin credentials

* Fix shell admin API key configuration
This commit is contained in:
Sipke Schoorstra 2026-05-20 20:58:03 +02:00 committed by GitHub
parent a860846b50
commit 746ba96a1f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
19 changed files with 424 additions and 93 deletions

View file

@ -73,7 +73,7 @@ This narrows the purpose of `SecurityRoot` and keeps it from being the default a
- **More responsibility for integrators**: Deployments must intentionally configure `DefaultAdminUser` or provide another trusted bootstrap path if no administrator exists yet.
- **Migration awareness**: Existing documentation and operational guidance that referenced `SecurityRoot` for user bootstrap must be updated.
- **Potential misconfiguration risk**: A weak or default admin password remains a deployment concern and must be handled carefully by integrators.
- **Potential misconfiguration risk**: A weak admin password remains a deployment concern and must be handled carefully by integrators.
### Neutral
@ -87,4 +87,3 @@ This narrows the purpose of `SecurityRoot` and keeps it from being the default a
- User-management endpoints should document only their permission requirements, not `SecurityRoot`.
- Authentication configuration may still use `SecurityRoot` for operations that intentionally remain root-level.
- Integrators should prefer environment-specific configuration for default admin credentials and rotate them according to their security practices.

View file

@ -85,14 +85,14 @@ docker pull elsaworkflows/elsa-server-and-studio-v3:latest
docker run -t -i -e ASPNETCORE_ENVIRONMENT='Development' -e HTTP_PORTS=8080 -e HTTP__BASEURL=http://localhost:13000 -p 13000:8080 elsaworkflows/elsa-server-and-studio-v3:latest
```
Default development login:
Default development login is available only when a development configuration explicitly provisions it:
```text
Username: admin
Password: password
```
Do not use default credentials in production.
Do not use development credentials in production.
## ASP.NET Middleware Order

View file

@ -3,5 +3,21 @@
"LogLevel": {
"Default": "Information"
}
},
"CShells": {
"Shells": {
"Default": {
"Features": {
"DefaultAdminUser": {
"AdminUserName": "admin",
"AdminPassword": "password",
"AdminRoleName": "admin",
"AdminRolePermissions": [
"*"
]
}
}
}
}
}
}

View file

@ -23,14 +23,6 @@
"Identity": {
"SigningKey": "CHANGE_ME_TO_A_SECURE_RANDOM_KEY"
},
"DefaultAdminUser": {
"AdminUserName": "admin",
"AdminPassword": "password",
"AdminRoleName": "admin",
"AdminRolePermissions": [
"*"
]
},
"FastEndpoints": {
"GlobalRoutePrefix": "elsa/api"
},

View file

@ -3,11 +3,8 @@
This project represents an Elsa application that hosts workflows and exposes API endpoints to manage & execute workflows.
## Secrets
The following are the secrets stored in hashed form in appsettings.json:
**API key**: `48587230567A646D394B435A6277734A-4802fa49-e91e-45e8-b00f-b5492377e20b`
**Admin user**: `admin`
**Admin password**: `password`
`appsettings.json` does not include production-usable default admin credentials or API keys. Configure initial users and applications through environment-specific configuration or a secret manager.
## OpenTelemetry (MacOS)
@ -25,4 +22,4 @@ OTEL_DOTNET_AUTO_TRACES_ADDITIONAL_SOURCES=Proto.Actor,Elsa.Workflows
OTEL_DOTNET_AUTO_TRACES_CONSOLE_EXPORTER_ENABLED=true
OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317
OTEL_EXPORTER_OTLP_PROTOCOL=grpc
OTEL_RESOURCE_ATTRIBUTES=service.name=Elsa Server,service.version=3.3.0,service.instance.id=instance-123,deployment.environment=development
OTEL_RESOURCE_ATTRIBUTES=service.name=Elsa Server,service.version=3.3.0,service.instance.id=instance-123,deployment.environment=development

View file

@ -0,0 +1,55 @@
{
"Identity": {
"Tokens": {
"SigningKey": "development-only-secret-signing-key-change-before-production"
},
"Users": [
{
"Id": "a2323f46-42db-4e15-af8b-94238717d817",
"Name": "admin",
"HashedPassword": "TfKzh9RLix6FPcCNeHLkGrysFu3bYxqzGqduNdi8v1U=",
"HashedPasswordSalt": "JEy9kBlhHCNsencitRHlGxmErmSgY+FVyMJulCH27Ds=",
"Roles": [
"admin"
],
"TenantId": ""
},
{
"Id": "b0cd0e506e713a9d",
"Name": "alice",
"Roles": [
"admin"
],
"HashedPassword": "8B0fFK/f/kk9GkVtzXfRJ2Y6cNyYVvLTfKouWcAcuPg=",
"HashedPasswordSalt": "xlNWvEng8fRvo0McyJopbRJ2MJ9NIYV/4IY5dOZeiiw=",
"TenantId": "tenant-2"
},
{
"Id": "789d5056cb3bb8e9",
"Name": "bob",
"Roles": [
"admin"
],
"HashedPassword": "GMHHvUw7RfIIBq+GAWZ/AlHR50TbgONYfGQ92Xge0EI=",
"HashedPasswordSalt": "V4uHKg1PFBc1bsngCQoptibOu14faj9obSn5B23h1wE=",
"TenantId": "tenant-1"
}
],
"Applications": [
{
"Id": "d57030226341448daff5a2935aba2d3f",
"Name": "Postman",
"Roles": [
"admin"
],
"ClientId": "HXr0Vzdm9KCZbwsJ",
"ClientSecret": "a<~QGGHTEA%u4;CU&'Wga5ED:_&Gd1C)",
"HashedApiKey": "Z5ClHs3mbzx8Pnw3+PxbMq8A/Y+VKMCCDTGYtax8JFM=",
"HashedApiKeySalt": "kBisa1X8FwBfN2zmyGMFRgIVVBleghhQAJ4WGyTkaD0=",
"HashedClientSecret": "jEv58d0SVbGQ3nBZM0lkzHghG4Y+lMKW80wipz+9vHk=",
"HashedClientSecretSalt": "xRKy14Ok1/tU3kLf/8V1fcbLIegy9vcM90Peu2tzohU=",
"TenantId": "default"
}
]
}
}

View file

@ -68,7 +68,7 @@
},
"Identity": {
"Tokens": {
"SigningKey": "sufficiently-large-secret-signing-key",
"SigningKey": "CHANGE_ME_TO_A_SECURE_RANDOM_KEY",
"AccessTokenLifetime": "1:00:00:00",
"RefreshTokenLifetime": "7:00:00:00"
},
@ -81,54 +81,6 @@
],
"TenantId": "default"
}
],
"Users": [
{
"Id": "a2323f46-42db-4e15-af8b-94238717d817",
"Name": "admin",
"HashedPassword": "TfKzh9RLix6FPcCNeHLkGrysFu3bYxqzGqduNdi8v1U=",
"HashedPasswordSalt": "JEy9kBlhHCNsencitRHlGxmErmSgY+FVyMJulCH27Ds=",
"Roles": [
"admin"
],
"TenantId": ""
},
{
"Id": "b0cd0e506e713a9d",
"Name": "alice",
"Roles": [
"admin"
],
"HashedPassword": "8B0fFK/f/kk9GkVtzXfRJ2Y6cNyYVvLTfKouWcAcuPg=",
"HashedPasswordSalt": "xlNWvEng8fRvo0McyJopbRJ2MJ9NIYV/4IY5dOZeiiw=",
"TenantId": "tenant-2"
},
{
"Id": "789d5056cb3bb8e9",
"Name": "bob",
"Roles": [
"admin"
],
"HashedPassword": "GMHHvUw7RfIIBq+GAWZ/AlHR50TbgONYfGQ92Xge0EI=",
"HashedPasswordSalt": "V4uHKg1PFBc1bsngCQoptibOu14faj9obSn5B23h1wE=",
"TenantId": "tenant-1"
}
],
"Applications": [
{
"Id": "d57030226341448daff5a2935aba2d3f",
"Name": "Postman",
"Roles": [
"admin"
],
"ClientId": "HXr0Vzdm9KCZbwsJ",
"ClientSecret": "a<~QGGHTEA%u4;CU&'Wga5ED:_&Gd1C)",
"HashedApiKey": "Z5ClHs3mbzx8Pnw3+PxbMq8A/Y+VKMCCDTGYtax8JFM=",
"HashedApiKeySalt": "kBisa1X8FwBfN2zmyGMFRgIVVBleghhQAJ4WGyTkaD0=",
"HashedClientSecret": "jEv58d0SVbGQ3nBZM0lkzHghG4Y+lMKW80wipz+9vHk=",
"HashedClientSecretSalt": "xRKy14Ok1/tU3kLf/8V1fcbLIegy9vcM90Peu2tzohU=",
"TenantId": "default"
}
]
},
"Scripting": {

View file

@ -4,6 +4,7 @@ using Elsa.Features.Abstractions;
using Elsa.Features.Attributes;
using Elsa.Features.Services;
using Elsa.Identity.Constants;
using Elsa.Identity.Options;
using Elsa.Identity.Providers;
using Elsa.Requirements;
using Microsoft.AspNetCore.Authentication;
@ -40,15 +41,44 @@ public class DefaultAuthenticationFeature : FeatureBase
/// <returns>The current <see cref="DefaultAuthenticationFeature"/>.</returns>
public DefaultAuthenticationFeature UseApiKeyAuthorization<T>() where T : class, IApiKeyProvider
{
ApiKeyProviderType = typeof(T);
_configureApiKeyAuthorization = builder => builder.AddApiKeyInAuthorizationHeader<T>();
return this;
}
/// <summary>
/// Configures the API key provider type to <see cref="AdminApiKeyProvider"/>.
/// Configures the API key provider type to <see cref="AdminApiKeyProvider"/>. The provider denies all keys unless configured.
/// </summary>
/// <returns>The current <see cref="DefaultAuthenticationFeature"/>.</returns>
public DefaultAuthenticationFeature UseAdminApiKey() => UseApiKeyAuthorization<AdminApiKeyProvider>();
/// <summary>
/// Configures the admin API key provider with an explicit API key.
/// </summary>
/// <param name="apiKey">The API key to accept.</param>
/// <returns>The current <see cref="DefaultAuthenticationFeature"/>.</returns>
public DefaultAuthenticationFeature UseAdminApiKey(string apiKey)
{
Services.Configure<AdminApiKeyOptions>(options => options.ApiKey = apiKey);
return UseAdminApiKey();
}
/// <summary>
/// Configures the admin API key provider with an explicit API key.
/// </summary>
/// <param name="configure">The admin API key options to configure.</param>
/// <returns>The current <see cref="DefaultAuthenticationFeature"/>.</returns>
public DefaultAuthenticationFeature UseAdminApiKey(Action<AdminApiKeyOptions> configure)
{
Services.Configure(configure);
return UseAdminApiKey();
}
/// <summary>
/// Enables the all-zero development admin API key. Do not use in production.
/// </summary>
/// <returns>The current <see cref="DefaultAuthenticationFeature"/>.</returns>
public DefaultAuthenticationFeature UseDevelopmentAdminApiKey() => UseAdminApiKey(AdminApiKeyProvider.DevelopmentApiKey);
/// <summary>
/// Disables the local host requirement for the security root policy.
@ -64,6 +94,7 @@ public class DefaultAuthenticationFeature : FeatureBase
public override void Apply()
{
Services.ConfigureOptions<ConfigureJwtBearerOptions>();
Services.Configure<AdminApiKeyOptions>(_ => { });
Services.AddIdentityTokenOptionsValidation();
var authBuilder = Services

View file

@ -102,7 +102,7 @@ public class IdentityFeature : FeatureBase
}
/// <summary>
/// Configures the feature to use <see cref="AdminUserProvider"/>.
/// Configures the feature to use <see cref="AdminUserProvider"/>. The provider denies all users unless configured.
/// </summary>
public void UseAdminUserProvider()
{
@ -110,6 +110,24 @@ public class IdentityFeature : FeatureBase
RoleProvider = sp => sp.GetRequiredService<AdminRoleProvider>();
}
/// <summary>
/// Configures the feature to use <see cref="AdminUserProvider"/> with an explicit admin user.
/// </summary>
public void UseAdminUserProvider(Action<AdminUserProviderOptions> configure)
{
UseAdminUserProvider();
Services.Configure(configure);
}
/// <summary>
/// Configures the feature to use the development admin user. Do not use in production.
/// </summary>
public void UseDevelopmentAdminUserProvider() => UseAdminUserProvider(options =>
{
options.UserName = "admin";
options.Password = "password";
});
/// <summary>
/// Configures the feature to use <see cref="StoreBasedApplicationProvider"/>.
/// </summary>
@ -149,6 +167,7 @@ public class IdentityFeature : FeatureBase
{
Services.Configure(TokenOptions);
Services.Configure(ApiKeyDefaults.AuthenticationScheme, ApiKeyOptions);
Services.Configure<AdminUserProviderOptions>(_ => { });
Services.Configure(UsersOptions);
Services.Configure(ApplicationsOptions);
Services.Configure(RolesOptions);

View file

@ -0,0 +1,22 @@
namespace Elsa.Identity.Options;
/// <summary>
/// Options for the built-in admin API key provider.
/// </summary>
public class AdminApiKeyOptions
{
/// <summary>
/// Gets or sets the API key to accept. Leave empty to disable the provider.
/// </summary>
public string ApiKey { get; set; } = "";
/// <summary>
/// Gets or sets the owner name assigned to the API key identity.
/// </summary>
public string OwnerName { get; set; } = "admin";
/// <summary>
/// Gets or sets the permissions assigned to the API key identity.
/// </summary>
public ICollection<string> Permissions { get; set; } = ["*"];
}

View file

@ -0,0 +1,27 @@
namespace Elsa.Identity.Options;
/// <summary>
/// Options for the built-in admin user provider.
/// </summary>
public class AdminUserProviderOptions
{
/// <summary>
/// Gets or sets the user ID assigned to the configured admin user.
/// </summary>
public string UserId { get; set; } = "admin";
/// <summary>
/// Gets or sets the admin user name to accept. Leave empty to disable the provider.
/// </summary>
public string UserName { get; set; } = "";
/// <summary>
/// Gets or sets the admin password to accept. Leave empty to disable the provider.
/// </summary>
public string Password { get; set; } = "";
/// <summary>
/// Gets or sets the roles assigned to the configured admin user.
/// </summary>
public ICollection<string> Roles { get; set; } = ["admin"];
}

View file

@ -1,27 +1,44 @@
using System.Security.Claims;
using AspNetCore.Authentication.ApiKey;
using Elsa.Identity.Models;
using Elsa.Identity.Options;
using Microsoft.Extensions.Options;
namespace Elsa.Identity.Providers;
/// <summary>
/// Provides an <see cref="IApiKey"/> with admin privileges for the default admin API key.
/// Provides an <see cref="IApiKey"/> with admin privileges for an explicitly configured admin API key.
/// </summary>
public class AdminApiKeyProvider : IApiKeyProvider
public class AdminApiKeyProvider(IOptions<AdminApiKeyOptions> options) : IApiKeyProvider
{
/// <summary>
/// The default admin API key.
/// Initializes a new instance of the <see cref="AdminApiKeyProvider"/> class with no accepted API key.
/// </summary>
public static readonly string DefaultApiKey = Guid.Empty.ToString();
[Obsolete("Use the options-based constructor. The built-in admin API key is disabled unless explicitly configured.")]
public AdminApiKeyProvider() : this(Microsoft.Extensions.Options.Options.Create(new AdminApiKeyOptions()))
{
}
/// <summary>
/// The all-zero development admin API key. Do not enable in production.
/// </summary>
public static readonly string DevelopmentApiKey = Guid.Empty.ToString();
/// <summary>
/// The legacy development admin API key.
/// </summary>
[Obsolete("Use DevelopmentApiKey. The built-in admin API key is disabled unless explicitly configured.")]
public static readonly string DefaultApiKey = DevelopmentApiKey;
/// <inheritdoc />
public Task<IApiKey?> ProvideAsync(string key)
{
if(key != DefaultApiKey)
var apiKeyOptions = options.Value;
if (string.IsNullOrWhiteSpace(apiKeyOptions.ApiKey) || key != apiKeyOptions.ApiKey)
return Task.FromResult<IApiKey?>(null);
var claims = new List<Claim> { new("permissions", "*") };
var apiKey = new ApiKey(key, "admin", claims);
var claims = apiKeyOptions.Permissions.Select(permission => new Claim("permissions", permission)).ToList();
var apiKey = new ApiKey(key, apiKeyOptions.OwnerName, claims);
return Task.FromResult<IApiKey>(apiKey)!;
}
}
}

View file

@ -1,36 +1,59 @@
using Elsa.Identity.Contracts;
using Elsa.Identity.Entities;
using Elsa.Identity.Models;
using Elsa.Identity.Options;
using Microsoft.Extensions.Options;
namespace Elsa.Identity.Providers;
/// <summary>
/// Represents a user provider that always returns a single admin user. This is useful for development purposes.
/// Represents a user provider that returns a single explicitly configured admin user. This is useful for development purposes.
/// </summary>
public class AdminUserProvider : IUserProvider
{
private readonly User _adminUser;
private readonly User? _adminUser;
/// <summary>
/// Initializes a new instance of the <see cref="AdminUserProvider"/> class.
/// </summary>
public AdminUserProvider(ISecretHasher secretHasher)
[Obsolete("Use the options-based constructor. The built-in admin user is disabled unless explicitly configured.")]
public AdminUserProvider(ISecretHasher secretHasher) : this(secretHasher, Microsoft.Extensions.Options.Options.Create(new AdminUserProviderOptions()))
{
var hashedSecret = secretHasher.HashSecret("password");
}
/// <summary>
/// Initializes a new instance of the <see cref="AdminUserProvider"/> class.
/// </summary>
public AdminUserProvider(ISecretHasher secretHasher, IOptions<AdminUserProviderOptions> options)
{
var providerOptions = options.Value;
if (string.IsNullOrWhiteSpace(providerOptions.UserName) || string.IsNullOrWhiteSpace(providerOptions.Password))
return;
var hashedSecret = secretHasher.HashSecret(providerOptions.Password);
_adminUser = new User
{
Id = "admin",
Name = "admin",
Id = providerOptions.UserId,
Name = providerOptions.UserName,
HashedPassword = hashedSecret.EncodeSecret(),
HashedPasswordSalt = hashedSecret.EncodeSalt(),
Roles = { "admin" }
Roles = providerOptions.Roles.ToList()
};
}
/// <inheritdoc />
public Task<User?> FindAsync(UserFilter filter, CancellationToken cancellationToken = default)
{
return Task.FromResult(_adminUser)!;
if (_adminUser == null)
return Task.FromResult<User?>(null);
if (filter.Id != null && filter.Id != _adminUser.Id)
return Task.FromResult<User?>(null);
if (filter.Name != null && filter.Name != _adminUser.Name)
return Task.FromResult<User?>(null);
return Task.FromResult<User?>(_adminUser);
}
}
}

View file

@ -27,7 +27,7 @@ Example (`appsettings.json`):
"DefaultAuthentication": {},
"DefaultAdminUser": {
"AdminUserName": "admin",
"AdminPassword": "password",
"AdminPassword": "REPLACE_WITH_SECURE_BOOTSTRAP_PASSWORD",
"AdminRoleName": "admin",
"AdminRolePermissions": ["*"]
}
@ -57,7 +57,7 @@ services.AddElsa(elsa =>
identity.UseDefaultAdmin(admin => admin
.WithAdminUserName("admin")
.WithAdminPassword("password")
.WithAdminPassword("REPLACE_WITH_SECURE_BOOTSTRAP_PASSWORD")
.WithAdminRoleName("admin")
.WithAdminRolePermissions(new List<string> { "*" }));
})
@ -68,13 +68,12 @@ services.AddElsa(elsa =>
You can also use the shorthand overload:
```csharp
identity.UseDefaultAdmin("admin", "password", "admin", new List<string> { "*" });
identity.UseDefaultAdmin("admin", "REPLACE_WITH_SECURE_BOOTSTRAP_PASSWORD", "admin", new List<string> { "*" });
```
### Operational notes
- The initializer is idempotent: existing admin role/user are not recreated.
- Do not keep development defaults (`admin` / `password`) in production.
- Do not keep development defaults in production.
- Prefer environment variables or a secret manager for admin credentials.
- After first bootstrap, rotate credentials according to your security policy.

View file

@ -2,7 +2,9 @@ using AspNetCore.Authentication.ApiKey;
using CShells.Features;
using Elsa.Extensions;
using Elsa.Identity.Constants;
using Elsa.Identity.Options;
using Elsa.Identity.Providers;
using Elsa.PackageManifest.Generator.Hints;
using Elsa.Requirements;
using JetBrains.Annotations;
using Microsoft.AspNetCore.Authentication.JwtBearer;
@ -28,10 +30,41 @@ public class DefaultAuthenticationFeature : IShellFeature
/// </summary>
public Type ApiKeyProviderType { get; set; } = typeof(DefaultApiKeyProvider);
/// <summary>
/// Gets or sets an explicit API key for <see cref="AdminApiKeyProvider"/>. Leave empty to disable the provider.
/// </summary>
[ManifestSetting(
DisplayName = "Admin API Key",
Description = "Explicit API key for the admin API key provider. Leave empty to disable built-in admin API key authentication.",
Category = "Security",
Secret = true,
Sensitive = true,
RestartRequired = true)]
public string AdminApiKey { get; set; } = "";
/// <summary>
/// Gets or sets whether the all-zero development admin API key should be enabled. Do not enable in production.
/// </summary>
[ManifestSetting(
DisplayName = "Use Development Admin API Key",
Description = "Enables the all-zero development admin API key. Do not enable in production.",
Category = "Security",
DefaultValue = "false",
RestartRequired = true)]
public bool UseDevelopmentAdminApiKey { get; set; }
public void ConfigureServices(IServiceCollection services)
{
var resolvedAdminApiKey = UseDevelopmentAdminApiKey ? AdminApiKeyProvider.DevelopmentApiKey : AdminApiKey;
if (!string.IsNullOrWhiteSpace(resolvedAdminApiKey))
ApiKeyProviderType = typeof(AdminApiKeyProvider);
services.ConfigureOptions<ConfigureJwtBearerOptions>();
services.AddIdentityTokenOptionsValidation();
services.Configure<AdminApiKeyOptions>(options =>
{
options.ApiKey = resolvedAdminApiKey;
});
var authBuilder = services
.AddAuthentication(MultiScheme)

View file

@ -34,6 +34,7 @@ public class IdentityFeature : IFastEndpointsShellFeature
options.Realm = "Elsa Workflows";
options.KeyName = "ApiKey";
});
services.Configure<AdminUserProviderOptions>(_ => { });
services.Configure<UsersOptions>(_ => { });
services.Configure<ApplicationsOptions>(_ => { });
services.Configure<RolesOptions>(_ => { });

View file

@ -74,7 +74,7 @@ public class WorkflowServer(Infrastructure infrastructure, string url) : WebAppl
elsa.AddWorkflowsFrom<WorkflowServer>();
elsa.AddActivitiesFrom<WorkflowServer>();
elsa.AddActivityHost<TestHostMethod>();
elsa.UseDefaultAuthentication(defaultAuthentication => defaultAuthentication.UseAdminApiKey());
elsa.UseDefaultAuthentication(defaultAuthentication => defaultAuthentication.UseDevelopmentAdminApiKey());
elsa.UseFluentStorageProvider(sp =>
{
var assemblyLocation = Assembly.GetExecutingAssembly().Location;
@ -177,6 +177,6 @@ public class WorkflowServer(Infrastructure infrastructure, string url) : WebAppl
protected override void ConfigureClient(HttpClient client)
{
client.DefaultRequestHeaders.Authorization = new("ApiKey", AdminApiKeyProvider.DefaultApiKey);
client.DefaultRequestHeaders.Authorization = new("ApiKey", AdminApiKeyProvider.DevelopmentApiKey);
}
}
}

View file

@ -0,0 +1,91 @@
using Elsa.Identity.Options;
using Elsa.Identity.Providers;
using Elsa.Identity.Services;
using OptionsFactory = Microsoft.Extensions.Options.Options;
namespace Elsa.Identity.UnitTests.Providers;
public class AdminCredentialProviderTests
{
private readonly DefaultSecretHasher _secretHasher = new();
[Fact]
public async Task AdminApiKeyProviderDeniesDevelopmentApiKeyByDefault()
{
var provider = CreateAdminApiKeyProvider();
var apiKey = await provider.ProvideAsync(AdminApiKeyProvider.DevelopmentApiKey);
Assert.Null(apiKey);
}
[Fact]
public async Task AdminApiKeyProviderAcceptsDevelopmentApiKeyWhenExplicitlyConfigured()
{
var provider = CreateAdminApiKeyProvider(options => options.ApiKey = AdminApiKeyProvider.DevelopmentApiKey);
var apiKey = await provider.ProvideAsync(AdminApiKeyProvider.DevelopmentApiKey);
Assert.NotNull(apiKey);
Assert.Equal("admin", apiKey.OwnerName);
Assert.Contains(apiKey.Claims, claim => claim.Type == "permissions" && claim.Value == "*");
}
[Theory]
[InlineData("admin")]
[InlineData("anyone")]
public async Task AdminUserProviderDeniesStaticPasswordByDefault(string userName)
{
var validator = CreateCredentialsValidator();
var user = await validator.ValidateAsync(userName, "password");
Assert.Null(user);
}
[Fact]
public async Task AdminUserProviderAcceptsDevelopmentCredentialsWhenExplicitlyConfigured()
{
var validator = CreateCredentialsValidator(options =>
{
options.UserName = "admin";
options.Password = "password";
});
var user = await validator.ValidateAsync("admin", "password");
Assert.NotNull(user);
Assert.Equal("admin", user.Name);
}
[Fact]
public async Task AdminUserProviderDeniesArbitraryUsernameWhenDevelopmentCredentialsAreConfigured()
{
var validator = CreateCredentialsValidator(options =>
{
options.UserName = "admin";
options.Password = "password";
});
var user = await validator.ValidateAsync("anyone", "password");
Assert.Null(user);
}
private static AdminApiKeyProvider CreateAdminApiKeyProvider(Action<AdminApiKeyOptions>? configure = null)
{
var options = new AdminApiKeyOptions();
configure?.Invoke(options);
return new(OptionsFactory.Create(options));
}
private DefaultUserCredentialsValidator CreateCredentialsValidator(Action<AdminUserProviderOptions>? configure = null)
{
var options = new AdminUserProviderOptions();
configure?.Invoke(options);
var userProvider = new AdminUserProvider(_secretHasher, OptionsFactory.Create(options));
return new(userProvider, _secretHasher);
}
}

View file

@ -0,0 +1,57 @@
using AspNetCore.Authentication.ApiKey;
using Elsa.Identity.Options;
using Elsa.Identity.Providers;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using ShellDefaultAuthenticationFeature = Elsa.Identity.ShellFeatures.DefaultAuthenticationFeature;
namespace Elsa.Identity.UnitTests.ShellFeatures;
public class DefaultAuthenticationFeatureTests
{
private readonly ShellDefaultAuthenticationFeature _feature = new();
private readonly ServiceCollection _services = new();
[Fact]
public void UsesDefaultApiKeyProviderWhenAdminApiKeyIsNotConfigured()
{
using var serviceProvider = Activate();
Assert.Equal(typeof(DefaultApiKeyProvider), _feature.ApiKeyProviderType);
Assert.Equal(string.Empty, serviceProvider.GetRequiredService<IOptions<AdminApiKeyOptions>>().Value.ApiKey);
}
[Fact]
public async Task UsesAdminApiKeyProviderWhenAdminApiKeyIsConfigured()
{
_feature.AdminApiKey = "configured-admin-api-key";
using var serviceProvider = Activate();
var provider = Assert.IsType<AdminApiKeyProvider>(serviceProvider.GetRequiredService<IApiKeyProvider>());
var apiKey = await provider.ProvideAsync(_feature.AdminApiKey);
Assert.Equal(typeof(AdminApiKeyProvider), _feature.ApiKeyProviderType);
Assert.NotNull(apiKey);
}
[Fact]
public async Task UsesAdminApiKeyProviderWhenDevelopmentAdminApiKeyIsEnabled()
{
_feature.UseDevelopmentAdminApiKey = true;
using var serviceProvider = Activate();
var provider = Assert.IsType<AdminApiKeyProvider>(serviceProvider.GetRequiredService<IApiKeyProvider>());
var apiKey = await provider.ProvideAsync(AdminApiKeyProvider.DevelopmentApiKey);
Assert.Equal(typeof(AdminApiKeyProvider), _feature.ApiKeyProviderType);
Assert.NotNull(apiKey);
}
private ServiceProvider Activate()
{
_feature.ConfigureServices(_services);
return _services.BuildServiceProvider();
}
}