Distinguish refresh tokens from API access tokens (#7509)
* Separate access and refresh token use * Address Greptile identity token feedback
This commit is contained in:
parent
435c25793b
commit
1da8709e2c
|
|
@ -66,6 +66,8 @@ Compare: [`3.5.3...3.6.0`](https://github.com/elsa-workflows/elsa-core/compare/3
|
|||
|
||||
## 🔧 Improvements
|
||||
|
||||
- **Identity token-use enforcement**: Access and refresh JWTs now carry a `token_use` claim. Default API bearer authentication accepts only access tokens, and `/identity/refresh-token` accepts only refresh tokens, preventing refresh tokens from being used as normal API bearer tokens. ([#7482](https://github.com/elsa-workflows/elsa-core/issues/7482))
|
||||
|
||||
- **`Elsa.Common` — distributed lock resilience**: A retry pipeline now wraps distributed lock acquisition and release to survive transient errors (network glitches, database timeouts). A new `ITransientExceptionDetector` service identifies retryable exceptions. ([ca268c16ad](https://github.com/elsa-workflows/elsa-core/commit/ca268c16ad)) ([#7161](https://github.com/elsa-workflows/elsa-core/pull/7161))
|
||||
|
||||
- **Tenant task manager with dependency ordering**: Tenant startup, background, and recurring task execution is now consolidated into a single `TenantTaskManager`. A new `[TaskDependency]` attribute and `TopologicalTaskSorter` ensure tasks execute in the correct dependency order. ([b577279321](https://github.com/elsa-workflows/elsa-core/commit/b577279321)) ([#7174](https://github.com/elsa-workflows/elsa-core/pull/7174))
|
||||
|
|
@ -162,4 +164,4 @@ Compare: [`3.5.3...3.6.0`](https://github.com/elsa-workflows/elsa-core/compare/3
|
|||
* Removed `Elsa.ServerAndStudio.Web` and related sample projects from solution. ([ecf5b390f1](https://github.com/elsa-workflows/elsa-core/commit/ecf5b390f1))
|
||||
* Updated documentation to reflect .NET 10.0 support and remove deprecated external dependency references. ([7add1030c4](https://github.com/elsa-workflows/elsa-core/commit/7add1030c4))
|
||||
* Added DeepWiki badge to README. ([b3ad57191a](https://github.com/elsa-workflows/elsa-core/commit/b3ad57191a))
|
||||
* Null safety and compiler warning fixes across multiple modules. ([490c8a2c9e](https://github.com/elsa-workflows/elsa-core/commit/490c8a2c9e), [2c0b3da5de](https://github.com/elsa-workflows/elsa-core/commit/2c0b3da5de)) ([#7050](https://github.com/elsa-workflows/elsa-core/pull/7050), [#7051](https://github.com/elsa-workflows/elsa-core/pull/7051))
|
||||
* Null safety and compiler warning fixes across multiple modules. ([490c8a2c9e](https://github.com/elsa-workflows/elsa-core/commit/490c8a2c9e), [2c0b3da5de](https://github.com/elsa-workflows/elsa-core/commit/2c0b3da5de)) ([#7050](https://github.com/elsa-workflows/elsa-core/pull/7050), [#7051](https://github.com/elsa-workflows/elsa-core/pull/7051))
|
||||
|
|
|
|||
|
|
@ -34,6 +34,8 @@ elsa
|
|||
|
||||
See [src/apps/Elsa.Server.Web/Program.cs](../../src/apps/Elsa.Server.Web/Program.cs).
|
||||
|
||||
Identity JWTs include a `token_use` claim. API bearer authentication accepts only access tokens (`token_use=access`), while `/identity/refresh-token` uses a dedicated refresh-token bearer scheme and accepts only refresh tokens (`token_use=refresh`). Clients should not send refresh tokens to normal API endpoints or access tokens to the refresh endpoint.
|
||||
|
||||
## Default Admin Bootstrap
|
||||
|
||||
The default admin bootstrap is documented in [src/modules/Elsa.Identity/README.md](../../src/modules/Elsa.Identity/README.md) and [ADR 0010](../adr/0010-default-admin-user-bootstrap-for-initial-identity-access.md).
|
||||
|
|
|
|||
|
|
@ -0,0 +1,19 @@
|
|||
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||
|
||||
namespace Elsa.Identity.Constants;
|
||||
|
||||
/// <summary>
|
||||
/// Authentication scheme names used by Elsa identity.
|
||||
/// </summary>
|
||||
public static class IdentityAuthenticationSchemes
|
||||
{
|
||||
/// <summary>
|
||||
/// The default JWT bearer scheme for API access tokens.
|
||||
/// </summary>
|
||||
public const string AccessToken = JwtBearerDefaults.AuthenticationScheme;
|
||||
|
||||
/// <summary>
|
||||
/// JWT bearer scheme used by the token refresh endpoint.
|
||||
/// </summary>
|
||||
public const string RefreshToken = "RefreshToken";
|
||||
}
|
||||
22
src/modules/Elsa.Identity/Constants/TokenUse.cs
Normal file
22
src/modules/Elsa.Identity/Constants/TokenUse.cs
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
namespace Elsa.Identity.Constants;
|
||||
|
||||
/// <summary>
|
||||
/// Constants for distinguishing identity token usage.
|
||||
/// </summary>
|
||||
public static class TokenUse
|
||||
{
|
||||
/// <summary>
|
||||
/// The claim type that stores the intended token usage.
|
||||
/// </summary>
|
||||
public const string ClaimType = "token_use";
|
||||
|
||||
/// <summary>
|
||||
/// Token use value for API bearer access tokens.
|
||||
/// </summary>
|
||||
public const string Access = "access";
|
||||
|
||||
/// <summary>
|
||||
/// Token use value for refresh tokens.
|
||||
/// </summary>
|
||||
public const string Refresh = "refresh";
|
||||
}
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
using Elsa.Extensions;
|
||||
using Elsa.Identity.Constants;
|
||||
using Elsa.Identity.Contracts;
|
||||
using Elsa.Identity.Models;
|
||||
using FastEndpoints;
|
||||
|
|
@ -26,6 +27,7 @@ internal class RefreshToken : EndpointWithoutRequest<LoginResponse>
|
|||
public override void Configure()
|
||||
{
|
||||
Post("/identity/refresh-token");
|
||||
AuthSchemes(IdentityAuthenticationSchemes.RefreshToken);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
|
|
@ -40,4 +42,4 @@ internal class RefreshToken : EndpointWithoutRequest<LoginResponse>
|
|||
|
||||
return new LoginResponse(true, tokens.AccessToken, tokens.RefreshToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ using Elsa.Extensions;
|
|||
using Elsa.Features.Abstractions;
|
||||
using Elsa.Features.Attributes;
|
||||
using Elsa.Features.Services;
|
||||
using Elsa.Identity.Constants;
|
||||
using Elsa.Identity.Providers;
|
||||
using Elsa.Requirements;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
|
|
@ -76,7 +77,8 @@ public class DefaultAuthenticationFeature : FeatureBase
|
|||
: JwtBearerDefaults.AuthenticationScheme;
|
||||
};
|
||||
})
|
||||
.AddJwtBearer();
|
||||
.AddJwtBearer()
|
||||
.AddJwtBearer(IdentityAuthenticationSchemes.RefreshToken);
|
||||
|
||||
_configureApiKeyAuthorization(authBuilder);
|
||||
|
||||
|
|
@ -86,4 +88,4 @@ public class DefaultAuthenticationFeature : FeatureBase
|
|||
Services.AddScoped<IApiKeyProvider>(sp => (IApiKeyProvider)sp.GetRequiredService(ApiKeyProviderType));
|
||||
Services.AddAuthorization(ConfigureAuthorizationOptions);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
using Elsa.Identity.Constants;
|
||||
using Elsa.Identity.Options;
|
||||
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
|
@ -26,6 +27,16 @@ public class ConfigureJwtBearerOptions : IConfigureNamedOptions<JwtBearerOptions
|
|||
/// <inheritdoc />
|
||||
public void Configure(string? name, JwtBearerOptions options)
|
||||
{
|
||||
_identityTokenOptions.Value.ConfigureJwtBearerOptions(options);
|
||||
var requiredTokenUse = name switch
|
||||
{
|
||||
IdentityAuthenticationSchemes.AccessToken => TokenUse.Access,
|
||||
IdentityAuthenticationSchemes.RefreshToken => TokenUse.Refresh,
|
||||
_ => null
|
||||
};
|
||||
|
||||
if (requiredTokenUse == null)
|
||||
return;
|
||||
|
||||
_identityTokenOptions.Value.ConfigureJwtBearerOptions(options, requiredTokenUse);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -52,7 +52,14 @@ public class IdentityTokenOptions
|
|||
/// Configures the <see cref="JwtBearerOptions"/> with the values from this instance.
|
||||
/// </summary>
|
||||
/// <param name="options">The options to configure.</param>
|
||||
public void ConfigureJwtBearerOptions(JwtBearerOptions options)
|
||||
public void ConfigureJwtBearerOptions(JwtBearerOptions options) => ConfigureJwtBearerOptions(options, TokenUse.Access);
|
||||
|
||||
/// <summary>
|
||||
/// Configures the <see cref="JwtBearerOptions"/> with the values from this instance.
|
||||
/// </summary>
|
||||
/// <param name="options">The options to configure.</param>
|
||||
/// <param name="requiredTokenUse">The required token usage claim value.</param>
|
||||
public void ConfigureJwtBearerOptions(JwtBearerOptions options, string requiredTokenUse)
|
||||
{
|
||||
options.TokenValidationParameters = new TokenValidationParameters
|
||||
{
|
||||
|
|
@ -63,10 +70,24 @@ public class IdentityTokenOptions
|
|||
LifetimeValidator = ValidateLifetime,
|
||||
NameClaimType = JwtRegisteredClaimNames.Name
|
||||
};
|
||||
options.Events ??= new JwtBearerEvents();
|
||||
var previousOnTokenValidated = options.Events.OnTokenValidated;
|
||||
options.Events.OnTokenValidated = async context =>
|
||||
{
|
||||
await previousOnTokenValidated(context);
|
||||
|
||||
if (context.Result?.Failure != null || context.Result?.None == true)
|
||||
return;
|
||||
|
||||
var tokenUse = context.Principal?.FindFirst(TokenUse.ClaimType)?.Value;
|
||||
|
||||
if (!string.Equals(tokenUse, requiredTokenUse, StringComparison.Ordinal))
|
||||
context.Fail($"The token is not a valid {requiredTokenUse} token.");
|
||||
};
|
||||
}
|
||||
|
||||
private static bool ValidateLifetime(DateTime? notBefore, DateTime? expires, SecurityToken securityToken, TokenValidationParameters validationParameters)
|
||||
{
|
||||
return expires != null && expires > DateTime.UtcNow;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
using System.Security.Claims;
|
||||
using Elsa.Common;
|
||||
using Elsa.Extensions;
|
||||
using Elsa.Identity.Constants;
|
||||
using Elsa.Identity.Contracts;
|
||||
using Elsa.Identity.Entities;
|
||||
using Elsa.Identity.Models;
|
||||
|
|
@ -48,20 +49,21 @@ public class DefaultAccessTokenIssuer(IRoleProvider roleProvider, ISystemClock s
|
|||
var now = systemClock.UtcNow;
|
||||
var accessTokenExpiresAt = now.Add(accessTokenLifetime);
|
||||
var refreshTokenExpiresAt = now.Add(refreshTokenLifetime);
|
||||
var accessToken = JwtBearer.CreateToken(options => ConfigureTokenOptions(options, accessTokenExpiresAt.UtcDateTime));
|
||||
var refreshToken = JwtBearer.CreateToken(options => ConfigureTokenOptions(options, refreshTokenExpiresAt.UtcDateTime));
|
||||
var accessToken = JwtBearer.CreateToken(options => ConfigureTokenOptions(options, accessTokenExpiresAt.UtcDateTime, TokenUse.Access));
|
||||
var refreshToken = JwtBearer.CreateToken(options => ConfigureTokenOptions(options, refreshTokenExpiresAt.UtcDateTime, TokenUse.Refresh));
|
||||
|
||||
return new IssuedTokens(accessToken, refreshToken);
|
||||
|
||||
void ConfigureTokenOptions(JwtCreationOptions options, DateTime expireAt)
|
||||
void ConfigureTokenOptions(JwtCreationOptions options, DateTime expireAt, string tokenUse)
|
||||
{
|
||||
options.SigningKey = signingKey;
|
||||
options.ExpireAt = expireAt;
|
||||
options.Issuer = issuer;
|
||||
options.Audience = audience;
|
||||
options.User.Claims.AddRange(claims);
|
||||
options.User.Claims.Add(new Claim(TokenUse.ClaimType, tokenUse));
|
||||
options.User.Permissions.AddRange(permissions);
|
||||
options.User.Roles.AddRange(roleNames);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
using AspNetCore.Authentication.ApiKey;
|
||||
using CShells.Features;
|
||||
using Elsa.Extensions;
|
||||
using Elsa.Identity.Constants;
|
||||
using Elsa.Identity.Providers;
|
||||
using Elsa.Requirements;
|
||||
using JetBrains.Annotations;
|
||||
|
|
@ -43,7 +44,8 @@ public class DefaultAuthenticationFeature : IShellFeature
|
|||
: JwtBearerDefaults.AuthenticationScheme;
|
||||
};
|
||||
})
|
||||
.AddJwtBearer();
|
||||
.AddJwtBearer()
|
||||
.AddJwtBearer(IdentityAuthenticationSchemes.RefreshToken);
|
||||
|
||||
// Configure API key authorization based on provider type
|
||||
if (ApiKeyProviderType == typeof(AdminApiKeyProvider))
|
||||
|
|
|
|||
|
|
@ -0,0 +1,6 @@
|
|||
namespace Elsa.Identity.UnitTests;
|
||||
|
||||
internal static class IdentityTokenTestConstants
|
||||
{
|
||||
public const string SigningKey = "test-signing-key-with-at-least-32-chars";
|
||||
}
|
||||
|
|
@ -0,0 +1,56 @@
|
|||
using Elsa.Extensions;
|
||||
using Elsa.Identity.Constants;
|
||||
using Elsa.Identity.Options;
|
||||
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||
|
||||
namespace Elsa.Identity.UnitTests.Options;
|
||||
|
||||
public class ConfigureJwtBearerOptionsTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task Configure_UsesAccessTokenValidationForDefaultBearerScheme()
|
||||
{
|
||||
var options = Configure(JwtBearerDefaults.AuthenticationScheme);
|
||||
var result = await IdentityTokenOptionsTokenUseTests.ValidateTokenUseAsync(options, actualTokenUse: TokenUse.Refresh);
|
||||
|
||||
Assert.NotNull(result.Failure);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Configure_UsesRefreshTokenValidationForRefreshTokenScheme()
|
||||
{
|
||||
var options = Configure(IdentityAuthenticationSchemes.RefreshToken);
|
||||
var result = await IdentityTokenOptionsTokenUseTests.ValidateTokenUseAsync(options, actualTokenUse: TokenUse.Access);
|
||||
|
||||
Assert.NotNull(result.Failure);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Configure_SkipsNonElsaManagedSchemes()
|
||||
{
|
||||
var configureOptions = CreateConfigureOptions();
|
||||
var options = new JwtBearerOptions();
|
||||
|
||||
configureOptions.Configure("ThirdPartyBearer", options);
|
||||
|
||||
Assert.Null(options.TokenValidationParameters.ValidIssuer);
|
||||
}
|
||||
|
||||
private static JwtBearerOptions Configure(string scheme)
|
||||
{
|
||||
var configureOptions = CreateConfigureOptions();
|
||||
var options = new JwtBearerOptions();
|
||||
|
||||
configureOptions.Configure(scheme, options);
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
private static ConfigureJwtBearerOptions CreateConfigureOptions()
|
||||
{
|
||||
return new ConfigureJwtBearerOptions(Microsoft.Extensions.Options.Options.Create(new IdentityTokenOptions
|
||||
{
|
||||
SigningKey = IdentityTokenTestConstants.SigningKey
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,166 @@
|
|||
using System.IdentityModel.Tokens.Jwt;
|
||||
using System.Security.Claims;
|
||||
using Elsa.Identity.Constants;
|
||||
using Elsa.Identity.Options;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
|
||||
namespace Elsa.Identity.UnitTests.Options;
|
||||
|
||||
public class IdentityTokenOptionsTokenUseTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task AccessTokenSchemeRejectsRefreshToken()
|
||||
{
|
||||
var result = await ValidateTokenUseAsync(requiredTokenUse: TokenUse.Access, actualTokenUse: TokenUse.Refresh);
|
||||
|
||||
Assert.NotNull(result.Failure);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RefreshTokenSchemeRejectsAccessToken()
|
||||
{
|
||||
var result = await ValidateTokenUseAsync(requiredTokenUse: TokenUse.Refresh, actualTokenUse: TokenUse.Access);
|
||||
|
||||
Assert.NotNull(result.Failure);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AccessTokenSchemeAcceptsAccessToken()
|
||||
{
|
||||
var result = await ValidateTokenUseAsync(requiredTokenUse: TokenUse.Access, actualTokenUse: TokenUse.Access);
|
||||
|
||||
Assert.Null(result.Failure);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RefreshTokenSchemeAcceptsRefreshToken()
|
||||
{
|
||||
var result = await ValidateTokenUseAsync(requiredTokenUse: TokenUse.Refresh, actualTokenUse: TokenUse.Refresh);
|
||||
|
||||
Assert.Null(result.Failure);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AccessTokenSchemeRejectsTokenWithMissingTokenUseClaim()
|
||||
{
|
||||
var result = await ValidateTokenUseAsync(requiredTokenUse: TokenUse.Access, actualTokenUse: null);
|
||||
|
||||
Assert.NotNull(result.Failure);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task OnTokenValidatedRunsPreviousHandlerBeforeTokenUseEnforcement()
|
||||
{
|
||||
var previousHandlerCalled = false;
|
||||
var identityOptions = new IdentityTokenOptions
|
||||
{
|
||||
SigningKey = IdentityTokenTestConstants.SigningKey
|
||||
};
|
||||
var jwtBearerOptions = new JwtBearerOptions
|
||||
{
|
||||
Events = new JwtBearerEvents
|
||||
{
|
||||
OnTokenValidated = context =>
|
||||
{
|
||||
previousHandlerCalled = true;
|
||||
context.Success();
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
};
|
||||
identityOptions.ConfigureJwtBearerOptions(jwtBearerOptions, TokenUse.Access);
|
||||
|
||||
var result = await ValidateTokenUseAsync(jwtBearerOptions, actualTokenUse: TokenUse.Refresh);
|
||||
|
||||
Assert.True(previousHandlerCalled);
|
||||
Assert.NotNull(result.Failure);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task OnTokenValidatedPreservesPreviousNoResult()
|
||||
{
|
||||
var identityOptions = new IdentityTokenOptions
|
||||
{
|
||||
SigningKey = IdentityTokenTestConstants.SigningKey
|
||||
};
|
||||
var jwtBearerOptions = new JwtBearerOptions
|
||||
{
|
||||
Events = new JwtBearerEvents
|
||||
{
|
||||
OnTokenValidated = context =>
|
||||
{
|
||||
context.NoResult();
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
};
|
||||
identityOptions.ConfigureJwtBearerOptions(jwtBearerOptions, TokenUse.Access);
|
||||
|
||||
var result = await ValidateTokenUseAsync(jwtBearerOptions, actualTokenUse: TokenUse.Access);
|
||||
|
||||
Assert.True(result.None);
|
||||
}
|
||||
|
||||
private static async Task<AuthenticateResult> ValidateTokenUseAsync(string requiredTokenUse, string? actualTokenUse)
|
||||
{
|
||||
var identityOptions = new IdentityTokenOptions
|
||||
{
|
||||
SigningKey = IdentityTokenTestConstants.SigningKey
|
||||
};
|
||||
var jwtBearerOptions = new JwtBearerOptions();
|
||||
identityOptions.ConfigureJwtBearerOptions(jwtBearerOptions, requiredTokenUse);
|
||||
return await ValidateTokenUseAsync(jwtBearerOptions, actualTokenUse);
|
||||
}
|
||||
|
||||
public static async Task<AuthenticateResult> ValidateTokenUseAsync(JwtBearerOptions jwtBearerOptions, string? actualTokenUse)
|
||||
{
|
||||
var identityOptions = new IdentityTokenOptions
|
||||
{
|
||||
SigningKey = IdentityTokenTestConstants.SigningKey
|
||||
};
|
||||
var principal = ValidateToken(CreateToken(identityOptions, actualTokenUse), jwtBearerOptions.TokenValidationParameters, out var securityToken);
|
||||
var context = new TokenValidatedContext(
|
||||
new DefaultHttpContext(),
|
||||
new AuthenticationScheme(JwtBearerDefaults.AuthenticationScheme, null, typeof(JwtBearerHandler)),
|
||||
jwtBearerOptions)
|
||||
{
|
||||
Principal = principal,
|
||||
SecurityToken = securityToken
|
||||
};
|
||||
|
||||
await jwtBearerOptions.Events.TokenValidated(context);
|
||||
|
||||
return context.Result ?? AuthenticateResult.Success(new AuthenticationTicket(principal, JwtBearerDefaults.AuthenticationScheme));
|
||||
}
|
||||
|
||||
private static string CreateToken(IdentityTokenOptions options, string? tokenUse)
|
||||
{
|
||||
var now = DateTime.UtcNow;
|
||||
var credentials = new SigningCredentials(options.CreateSecurityKey(), SecurityAlgorithms.HmacSha256);
|
||||
var claims = new List<Claim>
|
||||
{
|
||||
new(JwtRegisteredClaimNames.Name, "alice")
|
||||
};
|
||||
|
||||
if (tokenUse != null)
|
||||
claims.Add(new Claim(TokenUse.ClaimType, tokenUse));
|
||||
|
||||
var token = new JwtSecurityToken(
|
||||
issuer: options.Issuer,
|
||||
audience: options.Audience,
|
||||
claims: claims,
|
||||
notBefore: now,
|
||||
expires: now.AddMinutes(5),
|
||||
signingCredentials: credentials);
|
||||
|
||||
return new JwtSecurityTokenHandler().WriteToken(token);
|
||||
}
|
||||
|
||||
private static ClaimsPrincipal ValidateToken(string token, TokenValidationParameters tokenValidationParameters, out SecurityToken securityToken)
|
||||
{
|
||||
return new JwtSecurityTokenHandler().ValidateToken(token, tokenValidationParameters, out securityToken);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue