[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.
|
- **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.
|
- **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
|
### 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`.
|
- User-management endpoints should document only their permission requirements, not `SecurityRoot`.
|
||||||
- Authentication configuration may still use `SecurityRoot` for operations that intentionally remain root-level.
|
- 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.
|
- 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
|
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
|
```text
|
||||||
Username: admin
|
Username: admin
|
||||||
Password: password
|
Password: password
|
||||||
```
|
```
|
||||||
|
|
||||||
Do not use default credentials in production.
|
Do not use development credentials in production.
|
||||||
|
|
||||||
## ASP.NET Middleware Order
|
## ASP.NET Middleware Order
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,5 +3,21 @@
|
||||||
"LogLevel": {
|
"LogLevel": {
|
||||||
"Default": "Information"
|
"Default": "Information"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"CShells": {
|
||||||
|
"Shells": {
|
||||||
|
"Default": {
|
||||||
|
"Features": {
|
||||||
|
"DefaultAdminUser": {
|
||||||
|
"AdminUserName": "admin",
|
||||||
|
"AdminPassword": "password",
|
||||||
|
"AdminRoleName": "admin",
|
||||||
|
"AdminRolePermissions": [
|
||||||
|
"*"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -23,14 +23,6 @@
|
||||||
"Identity": {
|
"Identity": {
|
||||||
"SigningKey": "CHANGE_ME_TO_A_SECURE_RANDOM_KEY"
|
"SigningKey": "CHANGE_ME_TO_A_SECURE_RANDOM_KEY"
|
||||||
},
|
},
|
||||||
"DefaultAdminUser": {
|
|
||||||
"AdminUserName": "admin",
|
|
||||||
"AdminPassword": "password",
|
|
||||||
"AdminRoleName": "admin",
|
|
||||||
"AdminRolePermissions": [
|
|
||||||
"*"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"FastEndpoints": {
|
"FastEndpoints": {
|
||||||
"GlobalRoutePrefix": "elsa/api"
|
"GlobalRoutePrefix": "elsa/api"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -3,11 +3,8 @@
|
||||||
This project represents an Elsa application that hosts workflows and exposes API endpoints to manage & execute workflows.
|
This project represents an Elsa application that hosts workflows and exposes API endpoints to manage & execute workflows.
|
||||||
|
|
||||||
## Secrets
|
## Secrets
|
||||||
The following are the secrets stored in hashed form in appsettings.json:
|
|
||||||
|
|
||||||
**API key**: `48587230567A646D394B435A6277734A-4802fa49-e91e-45e8-b00f-b5492377e20b`
|
`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.
|
||||||
**Admin user**: `admin`
|
|
||||||
**Admin password**: `password`
|
|
||||||
|
|
||||||
## OpenTelemetry (MacOS)
|
## 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_DOTNET_AUTO_TRACES_CONSOLE_EXPORTER_ENABLED=true
|
||||||
OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317
|
OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317
|
||||||
OTEL_EXPORTER_OTLP_PROTOCOL=grpc
|
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": {
|
"Identity": {
|
||||||
"Tokens": {
|
"Tokens": {
|
||||||
"SigningKey": "sufficiently-large-secret-signing-key",
|
"SigningKey": "CHANGE_ME_TO_A_SECURE_RANDOM_KEY",
|
||||||
"AccessTokenLifetime": "1:00:00:00",
|
"AccessTokenLifetime": "1:00:00:00",
|
||||||
"RefreshTokenLifetime": "7:00:00:00"
|
"RefreshTokenLifetime": "7:00:00:00"
|
||||||
},
|
},
|
||||||
|
|
@ -81,54 +81,6 @@
|
||||||
],
|
],
|
||||||
"TenantId": "default"
|
"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": {
|
"Scripting": {
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ using Elsa.Features.Abstractions;
|
||||||
using Elsa.Features.Attributes;
|
using Elsa.Features.Attributes;
|
||||||
using Elsa.Features.Services;
|
using Elsa.Features.Services;
|
||||||
using Elsa.Identity.Constants;
|
using Elsa.Identity.Constants;
|
||||||
|
using Elsa.Identity.Options;
|
||||||
using Elsa.Identity.Providers;
|
using Elsa.Identity.Providers;
|
||||||
using Elsa.Requirements;
|
using Elsa.Requirements;
|
||||||
using Microsoft.AspNetCore.Authentication;
|
using Microsoft.AspNetCore.Authentication;
|
||||||
|
|
@ -40,15 +41,44 @@ public class DefaultAuthenticationFeature : FeatureBase
|
||||||
/// <returns>The current <see cref="DefaultAuthenticationFeature"/>.</returns>
|
/// <returns>The current <see cref="DefaultAuthenticationFeature"/>.</returns>
|
||||||
public DefaultAuthenticationFeature UseApiKeyAuthorization<T>() where T : class, IApiKeyProvider
|
public DefaultAuthenticationFeature UseApiKeyAuthorization<T>() where T : class, IApiKeyProvider
|
||||||
{
|
{
|
||||||
|
ApiKeyProviderType = typeof(T);
|
||||||
_configureApiKeyAuthorization = builder => builder.AddApiKeyInAuthorizationHeader<T>();
|
_configureApiKeyAuthorization = builder => builder.AddApiKeyInAuthorizationHeader<T>();
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <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>
|
/// </summary>
|
||||||
/// <returns>The current <see cref="DefaultAuthenticationFeature"/>.</returns>
|
/// <returns>The current <see cref="DefaultAuthenticationFeature"/>.</returns>
|
||||||
public DefaultAuthenticationFeature UseAdminApiKey() => UseApiKeyAuthorization<AdminApiKeyProvider>();
|
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>
|
/// <summary>
|
||||||
/// Disables the local host requirement for the security root policy.
|
/// Disables the local host requirement for the security root policy.
|
||||||
|
|
@ -64,6 +94,7 @@ public class DefaultAuthenticationFeature : FeatureBase
|
||||||
public override void Apply()
|
public override void Apply()
|
||||||
{
|
{
|
||||||
Services.ConfigureOptions<ConfigureJwtBearerOptions>();
|
Services.ConfigureOptions<ConfigureJwtBearerOptions>();
|
||||||
|
Services.Configure<AdminApiKeyOptions>(_ => { });
|
||||||
Services.AddIdentityTokenOptionsValidation();
|
Services.AddIdentityTokenOptionsValidation();
|
||||||
|
|
||||||
var authBuilder = Services
|
var authBuilder = Services
|
||||||
|
|
|
||||||
|
|
@ -102,7 +102,7 @@ public class IdentityFeature : FeatureBase
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <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>
|
/// </summary>
|
||||||
public void UseAdminUserProvider()
|
public void UseAdminUserProvider()
|
||||||
{
|
{
|
||||||
|
|
@ -110,6 +110,24 @@ public class IdentityFeature : FeatureBase
|
||||||
RoleProvider = sp => sp.GetRequiredService<AdminRoleProvider>();
|
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>
|
/// <summary>
|
||||||
/// Configures the feature to use <see cref="StoreBasedApplicationProvider"/>.
|
/// Configures the feature to use <see cref="StoreBasedApplicationProvider"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
@ -149,6 +167,7 @@ public class IdentityFeature : FeatureBase
|
||||||
{
|
{
|
||||||
Services.Configure(TokenOptions);
|
Services.Configure(TokenOptions);
|
||||||
Services.Configure(ApiKeyDefaults.AuthenticationScheme, ApiKeyOptions);
|
Services.Configure(ApiKeyDefaults.AuthenticationScheme, ApiKeyOptions);
|
||||||
|
Services.Configure<AdminUserProviderOptions>(_ => { });
|
||||||
Services.Configure(UsersOptions);
|
Services.Configure(UsersOptions);
|
||||||
Services.Configure(ApplicationsOptions);
|
Services.Configure(ApplicationsOptions);
|
||||||
Services.Configure(RolesOptions);
|
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 System.Security.Claims;
|
||||||
using AspNetCore.Authentication.ApiKey;
|
using AspNetCore.Authentication.ApiKey;
|
||||||
using Elsa.Identity.Models;
|
using Elsa.Identity.Models;
|
||||||
|
using Elsa.Identity.Options;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
|
||||||
namespace Elsa.Identity.Providers;
|
namespace Elsa.Identity.Providers;
|
||||||
|
|
||||||
/// <summary>
|
/// <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>
|
/// </summary>
|
||||||
public class AdminApiKeyProvider : IApiKeyProvider
|
public class AdminApiKeyProvider(IOptions<AdminApiKeyOptions> options) : IApiKeyProvider
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The default admin API key.
|
/// Initializes a new instance of the <see cref="AdminApiKeyProvider"/> class with no accepted API key.
|
||||||
/// </summary>
|
/// </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 />
|
/// <inheritdoc />
|
||||||
public Task<IApiKey?> ProvideAsync(string key)
|
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);
|
return Task.FromResult<IApiKey?>(null);
|
||||||
|
|
||||||
var claims = new List<Claim> { new("permissions", "*") };
|
var claims = apiKeyOptions.Permissions.Select(permission => new Claim("permissions", permission)).ToList();
|
||||||
var apiKey = new ApiKey(key, "admin", claims);
|
var apiKey = new ApiKey(key, apiKeyOptions.OwnerName, claims);
|
||||||
return Task.FromResult<IApiKey>(apiKey)!;
|
return Task.FromResult<IApiKey>(apiKey)!;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,36 +1,59 @@
|
||||||
using Elsa.Identity.Contracts;
|
using Elsa.Identity.Contracts;
|
||||||
using Elsa.Identity.Entities;
|
using Elsa.Identity.Entities;
|
||||||
using Elsa.Identity.Models;
|
using Elsa.Identity.Models;
|
||||||
|
using Elsa.Identity.Options;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
|
||||||
namespace Elsa.Identity.Providers;
|
namespace Elsa.Identity.Providers;
|
||||||
|
|
||||||
/// <summary>
|
/// <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>
|
/// </summary>
|
||||||
public class AdminUserProvider : IUserProvider
|
public class AdminUserProvider : IUserProvider
|
||||||
{
|
{
|
||||||
private readonly User _adminUser;
|
private readonly User? _adminUser;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="AdminUserProvider"/> class.
|
/// Initializes a new instance of the <see cref="AdminUserProvider"/> class.
|
||||||
/// </summary>
|
/// </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
|
_adminUser = new User
|
||||||
{
|
{
|
||||||
Id = "admin",
|
Id = providerOptions.UserId,
|
||||||
Name = "admin",
|
Name = providerOptions.UserName,
|
||||||
HashedPassword = hashedSecret.EncodeSecret(),
|
HashedPassword = hashedSecret.EncodeSecret(),
|
||||||
HashedPasswordSalt = hashedSecret.EncodeSalt(),
|
HashedPasswordSalt = hashedSecret.EncodeSalt(),
|
||||||
Roles = { "admin" }
|
Roles = providerOptions.Roles.ToList()
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public Task<User?> FindAsync(UserFilter filter, CancellationToken cancellationToken = default)
|
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": {},
|
"DefaultAuthentication": {},
|
||||||
"DefaultAdminUser": {
|
"DefaultAdminUser": {
|
||||||
"AdminUserName": "admin",
|
"AdminUserName": "admin",
|
||||||
"AdminPassword": "password",
|
"AdminPassword": "REPLACE_WITH_SECURE_BOOTSTRAP_PASSWORD",
|
||||||
"AdminRoleName": "admin",
|
"AdminRoleName": "admin",
|
||||||
"AdminRolePermissions": ["*"]
|
"AdminRolePermissions": ["*"]
|
||||||
}
|
}
|
||||||
|
|
@ -57,7 +57,7 @@ services.AddElsa(elsa =>
|
||||||
|
|
||||||
identity.UseDefaultAdmin(admin => admin
|
identity.UseDefaultAdmin(admin => admin
|
||||||
.WithAdminUserName("admin")
|
.WithAdminUserName("admin")
|
||||||
.WithAdminPassword("password")
|
.WithAdminPassword("REPLACE_WITH_SECURE_BOOTSTRAP_PASSWORD")
|
||||||
.WithAdminRoleName("admin")
|
.WithAdminRoleName("admin")
|
||||||
.WithAdminRolePermissions(new List<string> { "*" }));
|
.WithAdminRolePermissions(new List<string> { "*" }));
|
||||||
})
|
})
|
||||||
|
|
@ -68,13 +68,12 @@ services.AddElsa(elsa =>
|
||||||
You can also use the shorthand overload:
|
You can also use the shorthand overload:
|
||||||
|
|
||||||
```csharp
|
```csharp
|
||||||
identity.UseDefaultAdmin("admin", "password", "admin", new List<string> { "*" });
|
identity.UseDefaultAdmin("admin", "REPLACE_WITH_SECURE_BOOTSTRAP_PASSWORD", "admin", new List<string> { "*" });
|
||||||
```
|
```
|
||||||
|
|
||||||
### Operational notes
|
### Operational notes
|
||||||
|
|
||||||
- The initializer is idempotent: existing admin role/user are not recreated.
|
- 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.
|
- Prefer environment variables or a secret manager for admin credentials.
|
||||||
- After first bootstrap, rotate credentials according to your security policy.
|
- After first bootstrap, rotate credentials according to your security policy.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,9 @@ using AspNetCore.Authentication.ApiKey;
|
||||||
using CShells.Features;
|
using CShells.Features;
|
||||||
using Elsa.Extensions;
|
using Elsa.Extensions;
|
||||||
using Elsa.Identity.Constants;
|
using Elsa.Identity.Constants;
|
||||||
|
using Elsa.Identity.Options;
|
||||||
using Elsa.Identity.Providers;
|
using Elsa.Identity.Providers;
|
||||||
|
using Elsa.PackageManifest.Generator.Hints;
|
||||||
using Elsa.Requirements;
|
using Elsa.Requirements;
|
||||||
using JetBrains.Annotations;
|
using JetBrains.Annotations;
|
||||||
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||||
|
|
@ -28,10 +30,41 @@ public class DefaultAuthenticationFeature : IShellFeature
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public Type ApiKeyProviderType { get; set; } = typeof(DefaultApiKeyProvider);
|
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)
|
public void ConfigureServices(IServiceCollection services)
|
||||||
{
|
{
|
||||||
|
var resolvedAdminApiKey = UseDevelopmentAdminApiKey ? AdminApiKeyProvider.DevelopmentApiKey : AdminApiKey;
|
||||||
|
if (!string.IsNullOrWhiteSpace(resolvedAdminApiKey))
|
||||||
|
ApiKeyProviderType = typeof(AdminApiKeyProvider);
|
||||||
|
|
||||||
services.ConfigureOptions<ConfigureJwtBearerOptions>();
|
services.ConfigureOptions<ConfigureJwtBearerOptions>();
|
||||||
services.AddIdentityTokenOptionsValidation();
|
services.AddIdentityTokenOptionsValidation();
|
||||||
|
services.Configure<AdminApiKeyOptions>(options =>
|
||||||
|
{
|
||||||
|
options.ApiKey = resolvedAdminApiKey;
|
||||||
|
});
|
||||||
|
|
||||||
var authBuilder = services
|
var authBuilder = services
|
||||||
.AddAuthentication(MultiScheme)
|
.AddAuthentication(MultiScheme)
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,7 @@ public class IdentityFeature : IFastEndpointsShellFeature
|
||||||
options.Realm = "Elsa Workflows";
|
options.Realm = "Elsa Workflows";
|
||||||
options.KeyName = "ApiKey";
|
options.KeyName = "ApiKey";
|
||||||
});
|
});
|
||||||
|
services.Configure<AdminUserProviderOptions>(_ => { });
|
||||||
services.Configure<UsersOptions>(_ => { });
|
services.Configure<UsersOptions>(_ => { });
|
||||||
services.Configure<ApplicationsOptions>(_ => { });
|
services.Configure<ApplicationsOptions>(_ => { });
|
||||||
services.Configure<RolesOptions>(_ => { });
|
services.Configure<RolesOptions>(_ => { });
|
||||||
|
|
|
||||||
|
|
@ -74,7 +74,7 @@ public class WorkflowServer(Infrastructure infrastructure, string url) : WebAppl
|
||||||
elsa.AddWorkflowsFrom<WorkflowServer>();
|
elsa.AddWorkflowsFrom<WorkflowServer>();
|
||||||
elsa.AddActivitiesFrom<WorkflowServer>();
|
elsa.AddActivitiesFrom<WorkflowServer>();
|
||||||
elsa.AddActivityHost<TestHostMethod>();
|
elsa.AddActivityHost<TestHostMethod>();
|
||||||
elsa.UseDefaultAuthentication(defaultAuthentication => defaultAuthentication.UseAdminApiKey());
|
elsa.UseDefaultAuthentication(defaultAuthentication => defaultAuthentication.UseDevelopmentAdminApiKey());
|
||||||
elsa.UseFluentStorageProvider(sp =>
|
elsa.UseFluentStorageProvider(sp =>
|
||||||
{
|
{
|
||||||
var assemblyLocation = Assembly.GetExecutingAssembly().Location;
|
var assemblyLocation = Assembly.GetExecutingAssembly().Location;
|
||||||
|
|
@ -177,6 +177,6 @@ public class WorkflowServer(Infrastructure infrastructure, string url) : WebAppl
|
||||||
|
|
||||||
protected override void ConfigureClient(HttpClient client)
|
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