Refresh tokens (#3626)

* Remove elastic hosted service for alias/rollover

* Fix dependency issues

* Implement token refresh

* Update appsettings
This commit is contained in:
Sipke Schoorstra 2023-01-13 22:55:08 +01:00 committed by GitHub
parent 096206d392
commit 93297b6764
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
31 changed files with 206 additions and 146 deletions

View file

@ -14,7 +14,8 @@
"CreateDefaultAdmin": true,
"Tokens": {
"SigningKey": "secret-signing-key",
"Lifetime": "8:00:00"
"AccessTokenLifetime": "0:10:00",
"RefreshTokenLifetime": "1:00:00"
}
}
}

View file

@ -1,8 +1,17 @@
import {h} from '@stencil/core';
import {createStore} from '@stencil/store';
const {state, onChange} = createStore({
export interface AuthStore {
accessToken: string;
refreshToken: string;
name: string;
permissions: Array<string>;
signedIn: boolean;
}
const {state, onChange} = createStore<AuthStore>({
accessToken: null,
refreshToken: null,
name: null,
permissions: [],
signedIn: false

View file

@ -115,7 +115,6 @@ export class FlowchartComponent {
@Method()
async autoLayout(direction: LayoutDirection) {
debugger;
const dagreLayout = new DagreLayout({
type: 'dagre',
rankdir: direction,

View file

@ -41,11 +41,9 @@ export class LoginPage {
this.signedIn.emit();
const accessToken = loginResponse.accessToken;
const claims = jwt_decode<any>(accessToken);
const permissions = claims.permissions || [];
const name = claims.name || '';
const refreshToken = loginResponse.refreshToken;
const authContext = Container.get(AuthContext);
await authContext.signIn(name, permissions, accessToken, rememberMe);
await authContext.signinTokens(accessToken, refreshToken, rememberMe);
}
private renderError = () => {

View file

@ -1,6 +1,7 @@
export interface LoginResponse {
isAuthenticated: boolean;
accessToken: string;
refreshToken: string;
}
export interface SignedInArgs {

View file

@ -6,6 +6,8 @@ import {Container, Service} from "typedi";
import {StudioService, AuthContext, EventBus, ElsaClient, ElsaApiClientProvider} from "../../services";
import descriptorsStore from '../../data/descriptors-store';
import {SignedInArgs} from "./models";
import {AxiosInstance} from "axios";
import {LoginApi} from "./services";
@Service()
export class LoginPlugin implements Plugin {
@ -48,7 +50,7 @@ export class LoginPlugin implements Plugin {
private onHttpClientCreated = async (e) => {
const service: MiddlewareService = e.service;
const studioService = this.studioService;
const loginApi = Container.get(LoginApi);
service.register({
async onRequest(request) {
@ -56,16 +58,33 @@ export class LoginPlugin implements Plugin {
const token = authContext.getAccessToken();
if (!!token)
request.headers = {...request.headers, 'Authorization': `Bearer ${token}`};
request.headers = {...request.headers, Authorization: `Bearer ${token}`};
return request;
},
async onResponseError(error) {
if (error.response.status !== 401)
if (error.response.status !== 401 || error.response.config.hasRetriedRequest)
return;
studioService.show(() => <elsa-login-page onSignedIn={this.onSignedIn}/>);
const authContext = Container.get(AuthContext);
const loginResponse = await loginApi.refreshAccessToken(authContext.getRefreshToken());
if (loginResponse.isAuthenticated) {
await authContext.signinTokens(loginResponse.accessToken, loginResponse.refreshToken, true);
const t = await service.http({
...error.config,
hasRetriedRequest: true,
headers: {
...error.config.headers,
Authorization: `Bearer ${loginResponse.accessToken}`
}
});
return t;
}
}
});
};

View file

@ -1,14 +1,14 @@
import 'reflect-metadata';
import {Service} from "typedi";
import {ElsaApiClientProvider} from "../../services";
import {Container, Service} from "typedi";
import {ElsaApiClientProvider, EventBus, ServerSettings} from "../../services";
import {LoginResponse} from "./models";
import {AxiosError} from "axios";
import axios, {AxiosError, AxiosRequestConfig} from "axios";
import {EventTypes} from "../../models";
@Service()
export class LoginApi {
private provider: ElsaApiClientProvider;
constructor(provider: ElsaApiClientProvider) {
constructor(private provider: ElsaApiClientProvider, private serverSettings: ServerSettings) {
this.provider = provider;
}
@ -17,4 +17,17 @@ export class LoginApi {
const response = await httpClient.post<LoginResponse>(`identity/login`, {username, password},);
return response.data;
}
async refreshAccessToken(refreshToken: string): Promise<LoginResponse> {
const config: AxiosRequestConfig = {
baseURL: this.serverSettings.baseAddress,
headers: {
Authorization: `Bearer ${refreshToken}`
}
};
const httpClient = axios.create(config);
const response = await httpClient.post<LoginResponse>(`identity/refresh-token`);
return response.data;
}
}

View file

@ -228,7 +228,6 @@ export class WorkflowDefinitionEditor {
private onZoomToFit = async () => await this.flowchart.zoomToFit();
private onAutoLayout = async (direction: LayoutDirection) => {
debugger;
await this.flowchart.autoLayout(direction);
};

View file

@ -51,10 +51,8 @@ async function createHttpClient(baseAddress: string): Promise<AxiosInstance> {
const eventBus = Container.get(EventBus);
await eventBus.emit(EventTypes.HttpClient.ConfigCreated, this, {config});
const httpClient = axios.create(config);
const middlewareService = new MiddlewareService(httpClient);
await eventBus.emit(EventTypes.HttpClient.ClientCreated, this, {service: middlewareService, httpClient});
return httpClient;

View file

@ -4,6 +4,7 @@ import cookies from 'js-cookie';
import authStore from "../data/auth-store";
import {EventBus} from "./event-bus";
import {EventTypes} from "../models";
import jwt_decode from "jwt-decode";
export const AuthEventTypes = {
Unauthorized: 'auth:unauthorized'
@ -24,14 +25,23 @@ export class AuthContext {
authStore.permissions = authData.permissions;
authStore.signedIn = authData.signedIn;
authStore.accessToken = authData.accessToken;
authStore.refreshToken = authData.refreshToken;
}
}
async signIn(name: string, permissions: Array<string>, accessToken: string, createPersistentCookie: boolean) {
async signinTokens(accessToken: string, refreshToken: string, createPersistentCookie: boolean){
const claims = jwt_decode<any>(accessToken);
const permissions = claims.permissions || [];
const name = claims.name || '';
await this.signIn(name, permissions, accessToken, refreshToken, createPersistentCookie);
}
async signIn(name: string, permissions: Array<string>, accessToken: string, refreshToken: string, createPersistentCookie: boolean) {
authStore.name = name;
authStore.permissions = permissions;
authStore.signedIn = true;
authStore.accessToken = accessToken;
authStore.refreshToken = refreshToken;
const data = JSON.stringify(authStore);
sessionStorage.setItem('dashboard-session', data);
@ -47,7 +57,10 @@ export class AuthContext {
authStore.name = null;
authStore.permissions = [];
authStore.signedIn = false;
authStore.accessToken = false;
authStore.accessToken = null;
authStore.refreshToken = null;
sessionStorage.clear();
cookies.remove('dashboard-session');
await this.eventBus.emit(EventTypes.Auth.SignedOut)
}
@ -58,4 +71,8 @@ export class AuthContext {
getAccessToken() {
return authStore.accessToken;
}
getRefreshToken() {
return authStore.refreshToken;
}
}

View file

@ -92,7 +92,6 @@ function updatePortsAndEdgeOfNodeCouple(graph: Graph, sourceNode: Node<Node.Prop
if (edge != null) {
const sourcePortOfConnection = edge.data.sourcePort;
if(!optionsStore.enableFlexiblePorts && isNewCalculationNeededForInflexiblePort(graph, sourceNode, sourcePortOfConnection)){
debugger
const outgoingEdges = findOutgoingEdges(graph, sourceNode, sourcePortOfConnection);
const targetNodes = graph.getNodes().filter(node => outgoingEdges.map(edge => edge.data.target).includes(node.id));
const nodeCouplesWithPositions = calculatePositionsForInflexibleNode(sourceNode, targetNodes);
@ -102,7 +101,7 @@ function updatePortsAndEdgeOfNodeCouple(graph: Graph, sourceNode: Node<Node.Prop
});
return;
};
}
updatePortsAndEdge(graph, sourceNode, targetNode, portPositionOfSourceNode, portPositionOfTargetNode);
}

View file

@ -1,14 +1,9 @@
using Elastic.Clients.Elasticsearch;
using Elsa.Elasticsearch.Extensions;
using Elsa.Elasticsearch.HostedServices;
using Elsa.Elasticsearch.Modules.Management;
using Elsa.Elasticsearch.Modules.Runtime;
using Elsa.Elasticsearch.Options;
using Elsa.Elasticsearch.Services;
using Elsa.Features.Abstractions;
using Elsa.Features.Services;
using Elsa.Workflows.Management.Entities;
using Elsa.Workflows.Runtime.Entities;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
@ -30,12 +25,6 @@ public class ElasticsearchFeature : FeatureBase
/// </summary>
public Action<ElasticsearchOptions> Options { get; set; } = _ => { };
/// <inheritdoc />
public override void ConfigureHostedServices()
{
Module.ConfigureHostedService<ConfigureAliasesHostedService>(-2);
}
/// <inheritdoc />
public override void Apply()
{

View file

@ -1,62 +0,0 @@
using Elastic.Clients.Elasticsearch;
using Elastic.Clients.Elasticsearch.IndexManagement;
using Elsa.Elasticsearch.Options;
using Elsa.Elasticsearch.Services;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Options;
namespace Elsa.Elasticsearch.HostedServices;
/// <summary>
/// Configures aliases.
/// </summary>
public class ConfigureAliasesHostedService : IHostedService
{
private readonly ElasticsearchClient _client;
private readonly ElasticsearchOptions _options;
private readonly IEnumerable<IIndexConfiguration> _configurations;
/// <summary>
/// Constructor.
/// </summary>
public ConfigureAliasesHostedService(ElasticsearchClient client, IOptions<ElasticsearchOptions> options, IEnumerable<IIndexConfiguration> configurations)
{
_client = client;
_options = options.Value;
_configurations = configurations;
}
/// <inheritdoc />
public async Task StartAsync(CancellationToken cancellationToken)
{
await ConfigureClientAsync(cancellationToken);
}
/// <inheritdoc />
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
private async Task ConfigureClientAsync(CancellationToken cancellationToken)
{
foreach (var configuration in _configurations)
await configuration.ConfigureClientAsync(_client, cancellationToken);
foreach (var configuration in _configurations)
{
var alias = _options.GetIndexNameFor(configuration.DocumentType);
var indexName = configuration.IndexNamingStrategy.GenerateName(alias);
var indexExists = (await _client.Indices.ExistsAsync(indexName, cancellationToken)).Exists;
if (indexExists)
continue;
var response = await _client.Indices.CreateAsync(indexName, c => c
.Aliases(a => a.Add(alias, new Alias { IsWriteIndex = true })), cancellationToken);
if (response.IsValidResponse)
continue;
if (response.TryGetOriginalException(out var exception))
throw exception!;
}
}
}

View file

@ -33,6 +33,6 @@ public class EFCoreWorkflowManagementPersistenceFeature : PersistenceFeatureBase
base.Apply();
AddStore<WorkflowInstance, EFCoreWorkflowInstanceStore>();
AddStore<WorkflowInstance, EFCoreWorkflowInstanceStore>();
AddStore<WorkflowDefinition, EFCoreWorkflowDefinitionStore>();
}
}

View file

@ -1,6 +1,4 @@
using Elsa.Workflows.Runtime.Features;
using Elsa.Workflows.Runtime.Implementations;
using Microsoft.Extensions.DependencyInjection;
namespace Elsa.EntityFrameworkCore.Modules.Runtime;
@ -26,13 +24,4 @@ public static class Extensions
feature.Module.Configure(configure);
return feature;
}
/// <summary>
/// Configures the workflow runtime to send workflow state to the <see cref="AsyncWorkflowStateExporter"/>.
/// </summary>
public static WorkflowRuntimeFeature UseAsyncWorkflowStateExporter(this WorkflowRuntimeFeature feature)
{
feature.WorkflowStateExporter = sp => ActivatorUtilities.CreateInstance<AsyncWorkflowStateExporter>(sp);
return feature;
}
}

View file

@ -1,9 +1,10 @@
using Elsa.Identity.Services;
using Elsa.Identity.Models;
using Elsa.Identity.Services;
using FastEndpoints;
namespace Elsa.Identity.Endpoints.Login;
public class Login : Endpoint<Request, Response>
public class Login : Endpoint<Request, LoginResponse>
{
private readonly ICredentialsValidator _credentialsValidator;
private readonly IAccessTokenIssuer _tokenIssuer;
@ -14,21 +15,23 @@ public class Login : Endpoint<Request, Response>
_tokenIssuer = tokenIssuer;
}
/// <inheritdoc />
public override void Configure()
{
Post("/identity/login");
AllowAnonymous();
}
public override async Task<Response> ExecuteAsync(Request request, CancellationToken cancellationToken)
/// <inheritdoc />
public override async Task<LoginResponse> ExecuteAsync(Request request, CancellationToken cancellationToken)
{
var user = await _credentialsValidator.ValidateAsync(request.Username.Trim(), request.Password.Trim(), cancellationToken);
if (user == null)
return new Response(false, null);
return new LoginResponse(false, null, null);
var token = await _tokenIssuer.IssueTokenAsync(user, cancellationToken);
var tokens = await _tokenIssuer.IssueTokensAsync(user, cancellationToken);
return new Response(true, token);
return new LoginResponse(true, tokens.AccessToken, tokens.RefreshToken);
}
}

View file

@ -4,16 +4,4 @@ public class Request
{
public string Username { get; set; } = default!;
public string Password { get; set; } = default!;
}
public class Response
{
public Response(bool isAuthenticated, string? accessToken)
{
IsAuthenticated = isAuthenticated;
AccessToken = accessToken;
}
public bool IsAuthenticated { get; }
public string? AccessToken { get; }
}

View file

@ -0,0 +1,37 @@
using Elsa.Identity.Models;
using Elsa.Identity.Services;
using FastEndpoints;
namespace Elsa.Identity.Endpoints.RefreshToken;
public class RefreshToken : EndpointWithoutRequest<LoginResponse>
{
private readonly IUserStore _userStore;
private readonly IAccessTokenIssuer _tokenIssuer;
/// <inheritdoc />
public RefreshToken(IUserStore userStore, IAccessTokenIssuer tokenIssuer)
{
_userStore = userStore;
_tokenIssuer = tokenIssuer;
}
/// <inheritdoc />
public override void Configure()
{
Post("/identity/refresh-token");
}
/// <inheritdoc />
public override async Task<LoginResponse> ExecuteAsync(CancellationToken cancellationToken)
{
var user = await _userStore.FindAsync(User.Identity!.Name!, cancellationToken);
if (user == null)
return new LoginResponse(false, null, null);
var tokens = await _tokenIssuer.IssueTokensAsync(user, cancellationToken);
return new LoginResponse(true, tokens.AccessToken, tokens.RefreshToken);
}
}

View file

@ -28,7 +28,7 @@ public static class ModuleExtensions
{
Audience = audience,
Issuer = issuer,
Lifetime = tokenLifetime ?? TimeSpan.FromHours(1),
AccessTokenLifetime = tokenLifetime ?? TimeSpan.FromHours(1),
SigningKey = signingKey
});
return module;

View file

@ -20,10 +20,10 @@ public class DefaultAccessTokenIssuer : IAccessTokenIssuer
_identityOptions = identityOptions.Value;
}
public ValueTask<string> IssueTokenAsync(User user, CancellationToken cancellationToken = default)
public ValueTask<IssuedTokens> IssueTokensAsync(User user, CancellationToken cancellationToken = default)
{
var permissions = user.Roles.SelectMany(x => x.Permissions).ToList();
var (signingKey, issuer, audience, lifetime) = _identityOptions;
var (signingKey, issuer, audience, accessTokenLifetime, refreshTokenLifetime) = _identityOptions;
if (string.IsNullOrWhiteSpace(signingKey)) throw new Exception("No signing key configured");
if (string.IsNullOrWhiteSpace(issuer)) throw new Exception("No issuer configured");
@ -32,9 +32,13 @@ public class DefaultAccessTokenIssuer : IAccessTokenIssuer
var nameClaim = new Claim(JwtRegisteredClaimNames.Name, user.Name);
var claims = new[] { nameClaim };
var expiresAt = lifetime != null ? _systemClock.UtcNow.Add(lifetime.Value) : default(DateTimeOffset?);
var token = JWTBearer.CreateToken(signingKey, expiresAt?.DateTime, permissions, issuer: issuer, audience: audience, claims: claims);
var accessTokenExpiresAt = _systemClock.UtcNow.Add(accessTokenLifetime);
var refreshTokenExpiresAt = _systemClock.UtcNow.Add(refreshTokenLifetime);
var accessToken = JWTBearer.CreateToken(signingKey, accessTokenExpiresAt.UtcDateTime, permissions, issuer: issuer, audience: audience, claims: claims);
var refreshToken = JWTBearer.CreateToken(signingKey, refreshTokenExpiresAt.UtcDateTime, permissions, issuer: issuer, audience: audience, claims: claims);
return new(token);
return new (new IssuedTokens(accessToken, refreshToken));
}
}
}
public record IssuedTokens(string AccessToken, string RefreshToken);

View file

@ -0,0 +1,15 @@
namespace Elsa.Identity.Models;
public class LoginResponse
{
public LoginResponse(bool isAuthenticated, string? accessToken, string? refreshToken)
{
IsAuthenticated = isAuthenticated;
AccessToken = accessToken;
RefreshToken = refreshToken;
}
public bool IsAuthenticated { get; }
public string? AccessToken { get; }
public string? RefreshToken { get; }
}

View file

@ -1,3 +1,4 @@
using System.IdentityModel.Tokens.Jwt;
using System.Text;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;
@ -9,7 +10,8 @@ public class IdentityTokenOptions
public string SigningKey { get; set; }
public string Issuer { get; set; } = "http://elsa.api";
public string Audience { get; set; } = "http://elsa.api";
public TimeSpan? Lifetime { get; set; } = TimeSpan.FromHours(1);
public TimeSpan AccessTokenLifetime { get; set; } = TimeSpan.FromHours(1);
public TimeSpan RefreshTokenLifetime { get; set; } = TimeSpan.FromHours(2);
public SecurityKey CreateSecurityKey() => new SymmetricSecurityKey(Encoding.ASCII.GetBytes(SigningKey));
@ -18,18 +20,27 @@ public class IdentityTokenOptions
{
IssuerSigningKey = CreateSecurityKey(),
ValidAudience = Audience,
ValidIssuer = Issuer
ValidIssuer = Issuer,
ValidateLifetime = true,
LifetimeValidator = ValidateLifetime,
NameClaimType = JwtRegisteredClaimNames.Name
};
private bool ValidateLifetime(DateTime? notBefore, DateTime? expires, SecurityToken securityToken, TokenValidationParameters validationParameters)
{
return expires != null && expires > DateTime.UtcNow;
}
/// <summary>
/// Deconstructor.
/// </summary>
internal void Deconstruct(out string signingKey, out string issuer, out string audience, out TimeSpan? lifetime)
internal void Deconstruct(out string signingKey, out string issuer, out string audience, out TimeSpan accessTokenLifetime, out TimeSpan refreshTokenLifetime)
{
signingKey = SigningKey;
issuer = Issuer;
audience = Audience;
lifetime = Lifetime;
accessTokenLifetime = AccessTokenLifetime;
refreshTokenLifetime = RefreshTokenLifetime;
}
internal void CopyFrom(IdentityTokenOptions identityOptions)
@ -37,6 +48,7 @@ public class IdentityTokenOptions
SigningKey = identityOptions.SigningKey;
Audience = identityOptions.Audience;
Issuer = identityOptions.Issuer;
Lifetime = identityOptions.Lifetime;
AccessTokenLifetime = identityOptions.AccessTokenLifetime;
RefreshTokenLifetime = identityOptions.RefreshTokenLifetime;
}
}

View file

@ -1,8 +1,9 @@
using Elsa.Identity.Entities;
using Elsa.Identity.Implementations;
namespace Elsa.Identity.Services;
public interface IAccessTokenIssuer
{
ValueTask<string> IssueTokenAsync(User user, CancellationToken cancellationToken = default);
ValueTask<IssuedTokens> IssueTokensAsync(User user, CancellationToken cancellationToken = default);
}

View file

@ -29,6 +29,7 @@ namespace Elsa.Workflows.Management.Features;
[DependsOn(typeof(MediatorFeature))]
[DependsOn(typeof(SystemClockFeature))]
[DependsOn(typeof(WorkflowsFeature))]
[DependsOn(typeof(WorkflowDefinitionsFeature))]
public class WorkflowManagementFeature : FeatureBase
{
private const string PrimitivesCategory = "Primitives";

View file

@ -39,4 +39,10 @@ public static class ModuleExtensions
feature.Module.Configure(configure);
return feature;
}
public static WorkflowRuntimeFeature UseAsyncWorkflowStateExporter(this WorkflowRuntimeFeature feature, Action<AsyncWorkflowStateExporterFeature>? configure = default)
{
feature.Module.Configure(configure);
return feature;
}
}

View file

@ -0,0 +1,26 @@
using Elsa.Features.Abstractions;
using Elsa.Features.Attributes;
using Elsa.Features.Services;
using Elsa.Workflows.Management.Features;
using Elsa.Workflows.Runtime.Implementations;
using Microsoft.Extensions.DependencyInjection;
namespace Elsa.Workflows.Runtime.Features;
/// <summary>
/// Configures and enables <see cref="AsyncWorkflowStateExporter"/>.
/// </summary>
[DependsOn(typeof(WorkflowInstancesFeature))]
public class AsyncWorkflowStateExporterFeature : FeatureBase
{
/// <inheritdoc />
public AsyncWorkflowStateExporterFeature(IModule module) : base(module)
{
}
/// <inheritdoc />
public override void Configure()
{
Module.Configure<WorkflowRuntimeFeature>(workflowRuntime => workflowRuntime.WorkflowStateExporter = sp => ActivatorUtilities.CreateInstance<AsyncWorkflowStateExporter>(sp));
}
}

View file

@ -72,7 +72,7 @@ public class WorkflowRuntimeFeature : FeatureBase
/// <summary>
/// A factory that instantiates an <see cref="IWorkflowStateExporter"/>.
/// </summary>
public Func<IServiceProvider, IWorkflowStateExporter> WorkflowStateExporter { get; set; } = sp => sp.GetRequiredService<NoopWorkflowStateExporter>();
public Func<IServiceProvider, IWorkflowStateExporter> WorkflowStateExporter { get; set; } = ActivatorUtilities.GetServiceOrCreateInstance<NoopWorkflowStateExporter>;
/// <summary>
/// A factory that instantiates an <see cref="ITaskDispatcher"/>.
@ -145,8 +145,6 @@ public class WorkflowRuntimeFeature : FeatureBase
.AddWorkflowDefinitionProvider<ClrWorkflowDefinitionProvider>()
// Workflow state exporter.
.AddSingleton<NoopWorkflowStateExporter>()
.AddSingleton<AsyncWorkflowStateExporter>()
.AddSingleton(WorkflowStateExporter)
// Domain handlers.

View file

@ -37,7 +37,7 @@ builder.Services.AddElsa(elsa =>
{
identity.IdentityOptions.CreateDefaultAdmin = builder.Environment.IsDevelopment();
identity.TokenOptions.SigningKey = "secret-token-signing-key";
identity.TokenOptions.Lifetime = TimeSpan.FromDays(1);
identity.TokenOptions.AccessTokenLifetime = TimeSpan.FromDays(1);
});
// Use default authentication (JWT).

View file

@ -45,7 +45,7 @@ builder.Services.AddElsa(elsa =>
{
identity.IdentityOptions.CreateDefaultAdmin = builder.Environment.IsDevelopment();
identity.TokenOptions.SigningKey = "secret-token-signing-key";
identity.TokenOptions.Lifetime = TimeSpan.FromDays(1);
identity.TokenOptions.AccessTokenLifetime = TimeSpan.FromDays(1);
});
// Use default authentication (JWT).

View file

@ -40,7 +40,7 @@ builder.Services.AddElsa(elsa =>
{
identity.IdentityOptions.CreateDefaultAdmin = builder.Environment.IsDevelopment();
identity.TokenOptions.SigningKey = "secret-token-signing-key";
identity.TokenOptions.Lifetime = TimeSpan.FromDays(1);
identity.TokenOptions.AccessTokenLifetime = TimeSpan.FromDays(1);
});
// Use default authentication (JWT).

View file

@ -63,7 +63,7 @@ services
runtime.UseDefaultRuntime(dr => dr.UseEntityFrameworkCore(ef => ef.UseSqlite(sqliteConnectionString)));
runtime.UseExecutionLogRecords(d => d.UseEntityFrameworkCore(ef => ef.UseSqlite()));
runtime.UseExecutionLogRecords(e => e.UseEntityFrameworkCore(ef => ef.UseSqlite(sqliteConnectionString)));
runtime.WorkflowStateExporter = sp => sp.GetRequiredService<AsyncWorkflowStateExporter>();
runtime.UseAsyncWorkflowStateExporter();
})
.UseActivityDefinitions(feature => feature.UseEntityFrameworkCore(ef => ef.UseSqlite(sqliteConnectionString)))
.UseJobs(jobs => jobs.ConfigureOptions = options => options.WorkerCount = 10)