fix(auth): refresh-token rotation немедленно инвалидирует старый токен
Два бага в refresh-flow, из-за которых утёкший refresh-token давал доступ после ротации (auth-edge step03): 1. AuthorizationController прокидывал в новый principal только AuthorizationId, но не TokenId. Handler RedeemTokenEntry читает TokenId из подписываемого principal, чтобы пометить погашаемый refresh как Redeemed — без него старый токен оставался valid. 2. Даже после починки редемпшна OpenIddict по умолчанию даёт 30-секундный reuse-leeway: погашенный refresh ещё принимается в этом окне. Обнуляем окно (SetRefreshTokenReuseLeeway(TimeSpan.Zero)) — ротация инвалидирует старый refresh сразу. auth-edge: 10/10 (было 9/10). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
17a454cce5
commit
32729e72a3
|
|
@ -71,6 +71,20 @@ public async Task<IActionResult> Exchange()
|
|||
if (rejection is not null) return rejection;
|
||||
|
||||
var principal = await CreatePrincipal(user, request.GetScopes());
|
||||
// Прокидываем внутренние OpenIddict-claims погашаемого refresh-token
|
||||
// в новый principal. Ключевой — TokenId: handler RedeemTokenEntry
|
||||
// читает его из подписываемого principal, чтобы понять, какой именно
|
||||
// refresh-token пометить Redeemed. Без него ротация не гасит старый
|
||||
// токен — он остаётся valid и допускает повторное использование
|
||||
// (одна утечка refresh = вечный доступ). AuthorizationId связывает
|
||||
// новый токен с той же авторизацией, чтобы не плодить дубли.
|
||||
var tokenId = info.Principal?.GetTokenId();
|
||||
if (!string.IsNullOrEmpty(tokenId))
|
||||
principal.SetTokenId(tokenId);
|
||||
var authzId = info.Principal?.GetAuthorizationId();
|
||||
if (!string.IsNullOrEmpty(authzId))
|
||||
principal.SetAuthorizationId(authzId);
|
||||
|
||||
return SignIn(principal, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -101,6 +101,13 @@
|
|||
builder.Configuration["OpenIddict:AccessTokenLifetime"] ?? "01:00:00"));
|
||||
opts.SetRefreshTokenLifetime(TimeSpan.Parse(
|
||||
builder.Configuration["OpenIddict:RefreshTokenLifetime"] ?? "30.00:00:00"));
|
||||
// Rolling refresh включён по умолчанию: каждый refresh гасит старый
|
||||
// токен (Redeemed) и выдаёт новый. Но по умолчанию OpenIddict даёт
|
||||
// 30-секундный reuse-leeway — окно, в котором погашенный refresh ещё
|
||||
// принимается (для гонок/ретраев). Для розничной админки это дыра:
|
||||
// утёкший refresh остаётся рабочим 30с после ротации. Обнуляем окно —
|
||||
// ротация инвалидирует старый refresh немедленно.
|
||||
opts.SetRefreshTokenReuseLeeway(TimeSpan.Zero);
|
||||
// Явный Issuer = публичный URL админки. Без него OpenIddict
|
||||
// вычисляет issuer из текущего HTTP-запроса, что ломается за
|
||||
// nginx-прокси если Host/X-Forwarded-Proto не доходят. Берём
|
||||
|
|
|
|||
Loading…
Reference in a new issue