78 lines
3.6 KiB
C#
78 lines
3.6 KiB
C#
using System.Security.Claims;
|
|
using MAX.Framework.Security;
|
|
using Microsoft.Extensions.Options;
|
|
|
|
namespace Store.Users.Tests;
|
|
|
|
public sealed class PersistentSessionTests
|
|
{
|
|
[Fact]
|
|
public async Task TokensSurviveRestart_AndLogoutOnlyRevokesCurrentSession()
|
|
{
|
|
var path = Path.Combine(Path.GetTempPath(), $"sessions-{Guid.NewGuid():N}.db");
|
|
var options = Options.Create(new JwtOptions());
|
|
TokenResult first;
|
|
TokenResult second;
|
|
string firstJti;
|
|
try
|
|
{
|
|
using (var db = Open(path))
|
|
{
|
|
db.CodeFirst.SyncStructure<OnlineUserInfo>();
|
|
var store = new OnlineUserStore(db);
|
|
var tokens = new TokenService(options, store);
|
|
first = tokens.CreateToken([new Claim(JwtClaimTypes.UserName, "same")], "192.0.2.10");
|
|
second = tokens.CreateToken([new Claim(JwtClaimTypes.UserName, "same")], "2001:db8::1");
|
|
firstJti = (await tokens.ValidateTokenAsync(first.Token))!.FindFirst(JwtClaimTypes.Jti)!.Value;
|
|
Assert.NotNull(await tokens.ValidateTokenAsync(second.Token));
|
|
Assert.Equal(2, store.Count);
|
|
}
|
|
// 新 ORM、新存储和 TokenService,模拟进程重启后的数据库恢复。
|
|
using (var db = Open(path))
|
|
{
|
|
var store = new OnlineUserStore(db);
|
|
var tokens = new TokenService(options, store);
|
|
Assert.NotNull(await tokens.ValidateTokenAsync(first.Token));
|
|
Assert.NotNull(await tokens.ValidateTokenAsync(second.Token));
|
|
Assert.Contains(store.GetOnlineUsers(), x => x.LoginIp == "2001:db8::1");
|
|
store.SignOutSession("other", firstJti);
|
|
Assert.NotNull(await tokens.ValidateTokenAsync(first.Token));
|
|
store.SignOutSession("same", firstJti);
|
|
Assert.Null(await tokens.ValidateTokenAsync(first.Token));
|
|
Assert.NotNull(await tokens.ValidateTokenAsync(second.Token));
|
|
Assert.Single(store.GetOnlineUsers());
|
|
Assert.NotNull(db.Select<OnlineUserInfo>().Where(x => x.Jti == firstJti).First().RevokedAt);
|
|
}
|
|
using (var db = Open(path))
|
|
{
|
|
var store = new OnlineUserStore(db);
|
|
var tokens = new TokenService(options, store);
|
|
Assert.Null(await tokens.ValidateTokenAsync(first.Token));
|
|
Assert.NotNull(await tokens.ValidateTokenAsync(second.Token));
|
|
store.SignOut("same");
|
|
Assert.Null(await tokens.ValidateTokenAsync(second.Token));
|
|
}
|
|
}
|
|
finally { File.Delete(path); }
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ExpiredSessionsAreRejected_EvenWithinJwtClockTolerance()
|
|
{
|
|
using var db = Open(":memory:");
|
|
db.CodeFirst.SyncStructure<OnlineUserInfo>();
|
|
var store = new OnlineUserStore(db);
|
|
var tokens = new TokenService(Options.Create(new JwtOptions()), store);
|
|
var token = tokens.CreateToken([new Claim(JwtClaimTypes.UserName, "user")]);
|
|
db.Update<OnlineUserInfo>().Set(x => x.ExpiresAt, DateTime.UtcNow.AddSeconds(-1))
|
|
.Where(x => x.UserName == "user").ExecuteAffrows();
|
|
Assert.Null(await tokens.ValidateTokenAsync(token.Token));
|
|
Assert.Empty(store.GetOnlineUsers());
|
|
Assert.Equal(1, db.Select<OnlineUserInfo>().Count());
|
|
}
|
|
|
|
private static IFreeSql Open(string path) => new FreeSql.FreeSqlBuilder()
|
|
.UseConnectionString(FreeSql.DataType.Sqlite, $"Data Source={path};Pooling=false")
|
|
.Build();
|
|
}
|