[codex] Remove production-usable default admin credentials (#7500)
* Remove default admin credentials * Fix shell admin API key configuration
This commit is contained in:
parent
a860846b50
commit
746ba96a1f
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -3,5 +3,21 @@
|
|||
"LogLevel": {
|
||||
"Default": "Information"
|
||||
}
|
||||
},
|
||||
"CShells": {
|
||||
"Shells": {
|
||||
"Default": {
|
||||
"Features": {
|
||||
"DefaultAdminUser": {
|
||||
"AdminUserName": "admin",
|
||||
"AdminPassword": "password",
|
||||
"AdminRoleName": "admin",
|
||||
"AdminRolePermissions": [
|
||||
"*"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
55
src/apps/Elsa.Server.Web/appsettings.Development.json
Normal file
55
src/apps/Elsa.Server.Web/appsettings.Development.json
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
@ -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": {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
22
src/modules/Elsa.Identity/Options/AdminApiKeyOptions.cs
Normal file
22
src/modules/Elsa.Identity/Options/AdminApiKeyOptions.cs
Normal 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; } = ["*"];
|
||||
}
|
||||
|
|
@ -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"];
|
||||
}
|
||||
|
|
@ -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)!;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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>(_ => { });
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue