elsa-core/test/unit/Elsa.Identity.UnitTests/Options/IdentityTokenOptionsTokenUseTests.cs
Sipke Schoorstra 1da8709e2c
Distinguish refresh tokens from API access tokens (#7509)
* Separate access and refresh token use

* Address Greptile identity token feedback
2026-05-20 14:04:28 +02:00

167 lines
5.7 KiB
C#

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);
}
}