Refresh tokens (#3626)
* Remove elastic hosted service for alias/rollover * Fix dependency issues * Implement token refresh * Update appsettings
This commit is contained in:
parent
096206d392
commit
93297b6764
|
|
@ -14,7 +14,8 @@
|
|||
"CreateDefaultAdmin": true,
|
||||
"Tokens": {
|
||||
"SigningKey": "secret-signing-key",
|
||||
"Lifetime": "8:00:00"
|
||||
"AccessTokenLifetime": "0:10:00",
|
||||
"RefreshTokenLifetime": "1:00:00"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -115,7 +115,6 @@ export class FlowchartComponent {
|
|||
|
||||
@Method()
|
||||
async autoLayout(direction: LayoutDirection) {
|
||||
debugger;
|
||||
const dagreLayout = new DagreLayout({
|
||||
type: 'dagre',
|
||||
rankdir: direction,
|
||||
|
|
|
|||
|
|
@ -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 = () => {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
export interface LoginResponse {
|
||||
isAuthenticated: boolean;
|
||||
accessToken: string;
|
||||
refreshToken: string;
|
||||
}
|
||||
|
||||
export interface SignedInArgs {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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!;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -33,6 +33,6 @@ public class EFCoreWorkflowManagementPersistenceFeature : PersistenceFeatureBase
|
|||
base.Apply();
|
||||
|
||||
AddStore<WorkflowInstance, EFCoreWorkflowInstanceStore>();
|
||||
AddStore<WorkflowInstance, EFCoreWorkflowInstanceStore>();
|
||||
AddStore<WorkflowDefinition, EFCoreWorkflowDefinitionStore>();
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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; }
|
||||
}
|
||||
37
src/modules/Elsa.Identity/Endpoints/RefreshToken/Endpoint.cs
Normal file
37
src/modules/Elsa.Identity/Endpoints/RefreshToken/Endpoint.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
15
src/modules/Elsa.Identity/Models/LoginResponse.cs
Normal file
15
src/modules/Elsa.Identity/Models/LoginResponse.cs
Normal 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; }
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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).
|
||||
|
|
|
|||
|
|
@ -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).
|
||||
|
|
|
|||
|
|
@ -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).
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Reference in a new issue