%%%------------------------------------------------------------------- %%% @doc Бизнес-логика единой session-модели (refresh JWT + rotation). %%% Фаза 1: admin API. Фаза 2: user API. Фаза 3: client web + mobile. %%% @end %%%------------------------------------------------------------------- -module(logic_auth_session). -include("records.hrl"). -export([ issue_admin_tokens/2, refresh_admin/1, logout_admin/1, issue_user_tokens/2, issue_user_tokens/5, refresh_user/1, logout_user/1, list_user_sessions/1, revoke_user_session/2, revoke_other_user_sessions/2, normalize_user_client_type/1, normalize_device_name/1 ]). -define(USER_CLIENT_WEB, <<"web">>). -define(USER_CLIENT_MOBILE, <<"mobile">>). -define(DEVICE_NAME_MAX, 120). %%%------------------------------------------------------------------- %%% Admin (фаза 1) %%%------------------------------------------------------------------- -spec issue_admin_tokens(AdminId :: binary(), Role :: binary()) -> {ok, AccessToken :: binary(), RefreshToken :: binary(), SessionId :: binary()}. issue_admin_tokens(AdminId, Role) -> issue_tokens(AdminId, admin, <<"admin">>, <<>>, <<>>, Role, fun eventhub_auth:generate_admin_token/2, fun eventhub_auth:generate_admin_refresh_token/5). -spec refresh_admin(RefreshToken :: binary()) -> {ok, AccessToken :: binary(), NewRefreshToken :: binary(), SessionId :: binary()} | {error, atom()}. refresh_admin(RefreshToken) -> refresh_subject(RefreshToken, fun eventhub_auth:verify_admin_refresh_token/1, fun core_admin:get_by_id/1, admin, fun eventhub_auth:generate_admin_token/2, fun eventhub_auth:generate_admin_refresh_token/5). -spec logout_admin(RefreshToken :: binary()) -> ok | {error, atom()}. logout_admin(RefreshToken) -> logout_subject(RefreshToken, fun eventhub_auth:verify_admin_refresh_token/1). %%%------------------------------------------------------------------- %%% User (фаза 2 / 3) %%%------------------------------------------------------------------- -spec issue_user_tokens(UserId :: binary(), Role :: binary()) -> {ok, AccessToken :: binary(), RefreshToken :: binary(), SessionId :: binary()}. issue_user_tokens(UserId, Role) -> issue_user_tokens(UserId, Role, ?USER_CLIENT_WEB, <<>>, <<>>). -spec issue_user_tokens(UserId :: binary(), Role :: binary(), ClientType :: binary(), DeviceName :: binary(), UserAgent :: binary()) -> {ok, AccessToken :: binary(), RefreshToken :: binary(), SessionId :: binary()} | {error, invalid_client_type}. issue_user_tokens(UserId, Role, ClientType0, DeviceName0, UserAgent0) -> case normalize_user_client_type(ClientType0) of {ok, ClientType} -> DeviceName = normalize_device_name(DeviceName0), UserAgent = case UserAgent0 of U when is_binary(U) -> U; _ -> <<>> end, issue_tokens(UserId, user, ClientType, DeviceName, UserAgent, Role, fun eventhub_auth:generate_user_token/2, fun eventhub_auth:generate_user_refresh_token/5); {error, _} = Err -> Err end. -spec refresh_user(RefreshToken :: binary()) -> {ok, AccessToken :: binary(), NewRefreshToken :: binary(), SessionId :: binary()} | {error, atom()}. refresh_user(RefreshToken) -> refresh_subject(RefreshToken, fun eventhub_auth:verify_user_refresh_token/1, fun core_user:get_by_id/1, user, fun eventhub_auth:generate_user_token/2, fun eventhub_auth:generate_user_refresh_token/5). -spec logout_user(RefreshToken :: binary()) -> ok | {error, atom()}. logout_user(RefreshToken) -> logout_subject(RefreshToken, fun eventhub_auth:verify_user_refresh_token/1). -spec list_user_sessions(UserId :: binary()) -> [#auth_session{}]. list_user_sessions(UserId) -> core_auth_session:list_active_for_subject(UserId, user). -spec revoke_user_session(UserId :: binary(), SessionId :: binary()) -> ok | {error, not_found}. revoke_user_session(UserId, SessionId) -> case core_auth_session:get(SessionId) of {ok, #auth_session{subject_id = UserId, subject_type = user}} -> core_auth_session:revoke(SessionId); {ok, _} -> {error, not_found}; {error, not_found} -> {error, not_found} end. -spec revoke_other_user_sessions(UserId :: binary(), RefreshToken :: binary()) -> {ok, non_neg_integer()} | {error, atom()}. revoke_other_user_sessions(UserId, RefreshToken) -> case eventhub_auth:verify_user_refresh_token(RefreshToken) of {ok, Claims} -> case maps:get(<<"sub">>, Claims) of UserId -> SessionId = maps:get(<<"sid">>, Claims), core_auth_session:revoke_others_for_subject(UserId, user, SessionId); _ -> {error, forbidden} end; {error, _} = Err -> Err end. -spec normalize_user_client_type(term()) -> {ok, binary()} | {error, invalid_client_type}. normalize_user_client_type(undefined) -> {ok, ?USER_CLIENT_WEB}; normalize_user_client_type(null) -> {ok, ?USER_CLIENT_WEB}; normalize_user_client_type(<<>>) -> {ok, ?USER_CLIENT_WEB}; normalize_user_client_type(?USER_CLIENT_WEB) -> {ok, ?USER_CLIENT_WEB}; normalize_user_client_type(?USER_CLIENT_MOBILE) -> {ok, ?USER_CLIENT_MOBILE}; normalize_user_client_type(<<"WEB">>) -> {ok, ?USER_CLIENT_WEB}; normalize_user_client_type(<<"MOBILE">>) -> {ok, ?USER_CLIENT_MOBILE}; normalize_user_client_type(_) -> {error, invalid_client_type}. -spec normalize_device_name(term()) -> binary(). normalize_device_name(Name) when is_binary(Name) -> Trimmed = string:trim(Name), case byte_size(Trimmed) > ?DEVICE_NAME_MAX of true -> binary:part(Trimmed, 0, ?DEVICE_NAME_MAX); false -> Trimmed end; normalize_device_name(_) -> <<>>. %%%=================================================================== %%% Internal %%%=================================================================== logout_subject(RefreshToken, VerifyFn) -> case VerifyFn(RefreshToken) of {ok, Claims} -> SessionId = maps:get(<<"sid">>, Claims), case core_auth_session:revoke(SessionId) of ok -> ok; {error, not_found} -> ok; {error, _} = Err -> Err end; {error, _} = Err -> Err end. issue_tokens(SubjectId, SubjectType, ClientType, DeviceName, UserAgent, Role, GenAccess, GenRefresh) -> {ok, Session} = core_auth_session:create( SubjectId, SubjectType, ClientType, DeviceName, UserAgent), AccessToken = GenAccess(SubjectId, Role), {RefreshToken, _} = GenRefresh( Session#auth_session.session_id, Session#auth_session.family_id, Session#auth_session.current_jti, SubjectId, ClientType ), {ok, AccessToken, RefreshToken, Session#auth_session.session_id}. refresh_subject(RefreshToken, VerifyFn, GetSubjectFn, SubjectType, GenAccess, GenRefresh) -> case VerifyFn(RefreshToken) of {ok, Claims} -> do_refresh(Claims, GetSubjectFn, SubjectType, GenAccess, GenRefresh); Error -> Error end. do_refresh(Claims, GetSubjectFn, SubjectType, GenAccess, GenRefresh) -> SessionId = maps:get(<<"sid">>, Claims), Jti = maps:get(<<"jti">>, Claims), SubjectId = maps:get(<<"sub">>, Claims), FamilyId = maps:get(<<"fid">>, Claims), case core_auth_session:rotate(SessionId, Jti) of {ok, NewJti, Session} -> {ok, Subject} = GetSubjectFn(SubjectId), Role = subject_role(Subject, SubjectType), ClientType = Session#auth_session.client_type, AccessToken = GenAccess(SubjectId, Role), {NewRefresh, _} = GenRefresh(SessionId, FamilyId, NewJti, SubjectId, ClientType), {ok, AccessToken, NewRefresh, SessionId}; {error, reuse_detected} -> core_auth_session:revoke_family(FamilyId), {error, reuse_detected}; Error -> Error end. subject_role(#admin{role = Role}, admin) -> atom_to_binary(Role, utf8); subject_role(#user{role = Role}, user) -> atom_to_binary(Role, utf8).