feat(auth): list/revoke sessions with device_name and client_type. Fixes EventHub/EventHubBack#74
This commit is contained in:
@@ -5,55 +5,133 @@
|
||||
%%%-------------------------------------------------------------------
|
||||
-module(logic_auth_session).
|
||||
-include("records.hrl").
|
||||
-export([issue_admin_tokens/2, refresh_admin/1, logout_admin/1,
|
||||
issue_user_tokens/2, refresh_user/1, logout_user/1]).
|
||||
-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()}.
|
||||
{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,
|
||||
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()} |
|
||||
{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, <<"admin">>).
|
||||
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)
|
||||
%%% User (фаза 2 / 3)
|
||||
%%%-------------------------------------------------------------------
|
||||
|
||||
-spec issue_user_tokens(UserId :: binary(), Role :: binary()) ->
|
||||
{ok, AccessToken :: binary(), RefreshToken :: binary()}.
|
||||
{ok, AccessToken :: binary(), RefreshToken :: binary(), SessionId :: binary()}.
|
||||
issue_user_tokens(UserId, Role) ->
|
||||
issue_tokens(UserId, user, ?USER_CLIENT_WEB, Role, fun eventhub_auth:generate_user_token/2,
|
||||
fun eventhub_auth:generate_user_refresh_token/5).
|
||||
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()} |
|
||||
{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, ?USER_CLIENT_WEB).
|
||||
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
|
||||
%%%===================================================================
|
||||
@@ -71,8 +149,9 @@ logout_subject(RefreshToken, VerifyFn) ->
|
||||
Err
|
||||
end.
|
||||
|
||||
issue_tokens(SubjectId, SubjectType, ClientType, Role, GenAccess, GenRefresh) ->
|
||||
{ok, Session} = core_auth_session:create(SubjectId, SubjectType, ClientType),
|
||||
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,
|
||||
@@ -81,28 +160,29 @@ issue_tokens(SubjectId, SubjectType, ClientType, Role, GenAccess, GenRefresh) ->
|
||||
SubjectId,
|
||||
ClientType
|
||||
),
|
||||
{ok, AccessToken, RefreshToken}.
|
||||
{ok, AccessToken, RefreshToken, Session#auth_session.session_id}.
|
||||
|
||||
refresh_subject(RefreshToken, VerifyFn, GetSubjectFn, SubjectType, GenAccess, GenRefresh, ClientType) ->
|
||||
refresh_subject(RefreshToken, VerifyFn, GetSubjectFn, SubjectType, GenAccess, GenRefresh) ->
|
||||
case VerifyFn(RefreshToken) of
|
||||
{ok, Claims} ->
|
||||
do_refresh(Claims, GetSubjectFn, SubjectType, GenAccess, GenRefresh, ClientType);
|
||||
do_refresh(Claims, GetSubjectFn, SubjectType, GenAccess, GenRefresh);
|
||||
Error ->
|
||||
Error
|
||||
end.
|
||||
|
||||
do_refresh(Claims, GetSubjectFn, SubjectType, GenAccess, GenRefresh, ClientType) ->
|
||||
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, 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};
|
||||
{ok, AccessToken, NewRefresh, SessionId};
|
||||
{error, reuse_detected} ->
|
||||
core_auth_session:revoke_family(FamilyId),
|
||||
{error, reuse_detected};
|
||||
|
||||
Reference in New Issue
Block a user