feat!: drop the backend-side elevation windows; password and passkey sessions share capabilities, passkeys guard what nodes verify; the trust page lists added and removed passkeys [CI]
This commit is contained in:
1 parent
f704c66dd9
commit
4106f831dc
34 files changed
+191
-1364
No files matched your search
@@ -33,7 +33,6 @@ public sealed class PasskeyService
|
||||
|
||||
private readonly IAuthStore _store;
|
||||
private readonly AuthService _auth;
|
||||
private readonly ElevationService _elevation;
|
||||
private readonly TimeProvider _time;
|
||||
private readonly object _gate = new();
|
||||
private readonly Dictionary<string, PendingChallenge> _challenges = [];
|
||||
@@ -43,13 +42,10 @@ public sealed class PasskeyService
|
||||
private const string RegisterPurpose = "register";
|
||||
private const string LoginPurpose = "login";
|
||||
|
||||
private static string ElevatePurpose(ElevationClass elevation) => "elevate:" + Elevation.IdOf(elevation);
|
||||
|
||||
public PasskeyService(IAuthStore store, AuthService auth, ElevationService elevation, TimeProvider time)
|
||||
public PasskeyService(IAuthStore store, AuthService auth, TimeProvider time)
|
||||
{
|
||||
_store = store;
|
||||
_auth = auth;
|
||||
_elevation = elevation;
|
||||
_time = time;
|
||||
}
|
||||
|
||||
@@ -118,37 +114,6 @@ public sealed class PasskeyService
|
||||
: await _auth.CreateSessionForAsync(passkey.UserId, AuthMethods.Passkey, ct);
|
||||
}
|
||||
|
||||
/// <summary>提权的挑战:绑定用户和窗口种类,并列出这个用户的 passkey 让浏览器只选它们。没有 passkey 的用户没法提权。</summary>
|
||||
public async ValueTask<Result<RequestOptions>> BeginElevationAsync(Principal principal, ElevationClass elevation, PasskeyContext context, CancellationToken ct)
|
||||
{
|
||||
if (principal.UserId is not { } userId || principal.SessionId is null)
|
||||
{
|
||||
return Result<RequestOptions>.Fail(MoShiroErrors.Forbidden, "only a signed-in user can elevate; API keys carry their capabilities already");
|
||||
}
|
||||
|
||||
IReadOnlyList<PasskeyRecord> own = await _store.ListPasskeysAsync(userId, ct);
|
||||
if (own.Count == 0)
|
||||
{
|
||||
return Result<RequestOptions>.Fail(MoShiroErrors.ValidationFailed, "register a passkey first (Users, My passkeys)");
|
||||
}
|
||||
|
||||
return Result<RequestOptions>.Ok(new RequestOptions(Issue(ElevatePurpose(elevation), userId), context.RpId, 60_000, [.. own.Select(p => p.Id)]));
|
||||
}
|
||||
|
||||
/// <summary>验证提权的断言(必须是这个用户自己的 passkey)并给这个会话开窗口。失败一律 unauthorized。</summary>
|
||||
public async ValueTask<Result<ElevationWindow>> ElevateAsync(Principal principal, ElevationClass elevation, PasskeyAssertion request, PasskeyContext context, CancellationToken ct)
|
||||
{
|
||||
if (principal.UserId is not { } userId || principal.SessionId is not { } sessionId)
|
||||
{
|
||||
return Result<ElevationWindow>.Fail(MoShiroErrors.Forbidden, "only a signed-in user can elevate; API keys carry their capabilities already");
|
||||
}
|
||||
|
||||
PasskeyRecord? passkey = await VerifyAsync(request, context, ElevatePurpose(elevation), userId, ct);
|
||||
return passkey is null || passkey.UserId != userId
|
||||
? Result<ElevationWindow>.Fail(MoShiroErrors.Unauthorized, "invalid credentials")
|
||||
: Result<ElevationWindow>.Ok(new ElevationWindow(elevation, _elevation.Grant(sessionId, elevation)));
|
||||
}
|
||||
|
||||
/// <summary>验证一份断言:挑战(一次有效、种类和用户对得上)、签名、计数器;通过则记下使用并返回那个 passkey。</summary>
|
||||
private async ValueTask<PasskeyRecord?> VerifyAsync(PasskeyAssertion request, PasskeyContext context, string purpose, string? userId, CancellationToken ct)
|
||||
{
|
||||
|
||||
Reference in new issue
Block a user