diff --git a/src/food-market.api/Controllers/AuthorizationController.cs b/src/food-market.api/Controllers/AuthorizationController.cs index bab91df..eb032ba 100644 --- a/src/food-market.api/Controllers/AuthorizationController.cs +++ b/src/food-market.api/Controllers/AuthorizationController.cs @@ -71,6 +71,20 @@ public async Task 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); } diff --git a/src/food-market.api/Program.cs b/src/food-market.api/Program.cs index bc42e06..7f6f297 100644 --- a/src/food-market.api/Program.cs +++ b/src/food-market.api/Program.cs @@ -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 не доходят. Берём