Реализовать кластер-безопасные admin refresh-сессии (фаза 1, #23).

Единая auth_session с refresh JWT, rotation и reuse detection; CI/CD временно на workflow_dispatch.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-07-07 15:30:27 +03:00
parent efd67ef1e3
commit 9de57bd52c
13 changed files with 525 additions and 45 deletions
+62
View File
@@ -0,0 +1,62 @@
%%%-------------------------------------------------------------------
%%% @doc Бизнес-логика единой session-модели (refresh JWT + rotation).
%%% Фаза 1: admin API. Фазы 23: user / client web / mobile.
%%% @end
%%%-------------------------------------------------------------------
-module(logic_auth_session).
-include("records.hrl").
-export([issue_admin_tokens/2, refresh_admin/1]).
%%%-------------------------------------------------------------------
%%% @doc Выдать пару access + refresh JWT при входе администратора.
%%% @end
%%%-------------------------------------------------------------------
-spec issue_admin_tokens(AdminId :: binary(), Role :: binary()) ->
{ok, AccessToken :: binary(), RefreshToken :: binary()}.
issue_admin_tokens(AdminId, Role) ->
{ok, Session} = core_auth_session:create(AdminId, admin, <<"admin">>),
AccessToken = eventhub_auth:generate_admin_token(AdminId, Role),
{RefreshToken, _} = eventhub_auth:generate_admin_refresh_token(
Session#auth_session.session_id,
Session#auth_session.family_id,
Session#auth_session.current_jti,
AdminId,
<<"admin">>
),
{ok, AccessToken, RefreshToken}.
%%%-------------------------------------------------------------------
%%% @doc Обновить пару токенов администратора по refresh JWT.
%%% @end
%%%-------------------------------------------------------------------
-spec refresh_admin(RefreshToken :: binary()) ->
{ok, AccessToken :: binary(), NewRefreshToken :: binary()} |
{error, atom()}.
refresh_admin(RefreshToken) ->
case eventhub_auth:verify_admin_refresh_token(RefreshToken) of
{ok, Claims} ->
do_refresh_admin(Claims);
Error ->
Error
end.
do_refresh_admin(Claims) ->
SessionId = maps:get(<<"sid">>, Claims),
Jti = maps:get(<<"jti">>, Claims),
AdminId = maps:get(<<"sub">>, Claims),
FamilyId = maps:get(<<"fid">>, Claims),
case core_auth_session:rotate(SessionId, Jti) of
{ok, NewJti, _Session} ->
{ok, Admin} = core_admin:get_by_id(AdminId),
Role = atom_to_binary(Admin#admin.role, utf8),
AccessToken = eventhub_auth:generate_admin_token(AdminId, Role),
{NewRefresh, _} = eventhub_auth:generate_admin_refresh_token(
SessionId, FamilyId, NewJti, AdminId, <<"admin">>
),
{ok, AccessToken, NewRefresh};
{error, reuse_detected} ->
core_auth_session:revoke_family(FamilyId),
{error, reuse_detected};
Error ->
Error
end.