feat(auth): list/revoke sessions with device_name and client_type. Fixes EventHub/EventHubBack#74
CI / test (push) Failing after 10m52s
CI / deploy-ift (push) Has been skipped
CI / e2e-ift (push) Has been skipped
CI / deploy-stage (push) Has been skipped
CI / e2e-stage (push) Has been skipped

This commit is contained in:
2026-08-16 18:30:05 +03:00
parent 1c701a5254
commit 0690020e81
16 changed files with 482 additions and 62 deletions
+1 -1
View File
@@ -31,7 +31,7 @@ admin_refresh_test_() ->
]}.
test_refresh_ok() ->
{ok, _Access, Refresh} = logic_auth_session:issue_admin_tokens(?ADMIN_ID, <<"admin">>),
{ok, _Access, Refresh, _} = logic_auth_session:issue_admin_tokens(?ADMIN_ID, <<"admin">>),
{Status, _, Body} = eh_test_support:call(admin_handler_refresh, #{
method => <<"POST">>,
path => <<"/v1/admin/refresh">>,
+23 -1
View File
@@ -28,9 +28,11 @@ cleanup(_) ->
core_auth_session_test_() ->
{foreach, fun setup/0, fun cleanup/1, [
{"Create session", fun test_create/0},
{"Create with device", fun test_create_device/0},
{"Rotate session updates jti", fun test_rotate/0},
{"Reuse detection", fun test_reuse/0},
{"Revoke family", fun test_revoke_family/0}
{"Revoke family", fun test_revoke_family/0},
{"List active and revoke others", fun test_list_and_revoke_others/0}
]}.
test_create() ->
@@ -38,9 +40,16 @@ test_create() ->
?assertEqual(<<"admin1">>, Session#auth_session.subject_id),
?assertEqual(admin, Session#auth_session.subject_type),
?assertEqual(false, Session#auth_session.revoked),
?assertEqual(<<>>, Session#auth_session.device_name),
{ok, Loaded} = core_auth_session:get(Session#auth_session.session_id),
?assertEqual(Session#auth_session.session_id, Loaded#auth_session.session_id).
test_create_device() ->
{ok, Session} = core_auth_session:create(
<<"u1">>, user, <<"web">>, <<"Chrome · Win">>, <<"Mozilla">>),
?assertEqual(<<"Chrome · Win">>, Session#auth_session.device_name),
?assertEqual(<<"Mozilla">>, Session#auth_session.user_agent).
test_rotate() ->
{ok, Session} = core_auth_session:create(<<"admin1">>, admin, <<"admin">>),
OldJti = Session#auth_session.current_jti,
@@ -61,3 +70,16 @@ test_revoke_family() ->
ok = core_auth_session:revoke_family(S1#auth_session.family_id),
?assertMatch({error, revoked},
core_auth_session:rotate(S1#auth_session.session_id, S1#auth_session.current_jti)).
test_list_and_revoke_others() ->
{ok, Keep} = core_auth_session:create(<<"u1">>, user, <<"web">>, <<"A">>, <<"ua">>),
{ok, Other} = core_auth_session:create(<<"u1">>, user, <<"mobile">>, <<"B">>, <<"ub">>),
Active = core_auth_session:list_active_for_subject(<<"u1">>, user),
?assertEqual(2, length(Active)),
{ok, 1} = core_auth_session:revoke_others_for_subject(
<<"u1">>, user, Keep#auth_session.session_id),
Left = core_auth_session:list_active_for_subject(<<"u1">>, user),
?assertEqual(1, length(Left)),
?assertEqual(Keep#auth_session.session_id, (hd(Left))#auth_session.session_id),
{ok, Revoked} = core_auth_session:get(Other#auth_session.session_id),
?assertEqual(true, Revoked#auth_session.revoked).
+3 -1
View File
@@ -110,7 +110,9 @@ write_auth_session(Id, Revoked, ExpiresAt, CreatedAt) ->
expires_at = ExpiresAt,
revoked = Revoked,
created_at = CreatedAt,
updated_at = CreatedAt
updated_at = CreatedAt,
device_name = <<>>,
user_agent = <<>>
}).
write_admin_audit(Id, Timestamp) ->
+61 -11
View File
@@ -39,7 +39,9 @@ logic_auth_session_test_() ->
{"Issue and refresh user tokens", {timeout, 120, fun test_issue_and_refresh_user/0}},
{"Refresh user reuse revokes family", {timeout, 120, fun test_user_reuse_revokes/0}},
{"Logout user then refresh fails", {timeout, 120, fun test_logout_user/0}},
{"Logout admin then refresh fails", {timeout, 120, fun test_logout_admin/0}}
{"Logout admin then refresh fails", {timeout, 120, fun test_logout_admin/0}},
{"Mobile device meta and list", {timeout, 120, fun test_mobile_device_and_list/0}},
{"Revoke others keeps current", {timeout, 120, fun test_revoke_others/0}}
]}.
test_issue_and_refresh_admin() ->
@@ -54,10 +56,12 @@ test_issue_and_refresh_admin() ->
ok = meck:expect(core_admin, get_by_id, fun(Id) ->
?assertEqual(AdminId, Id), {ok, Admin}
end),
{ok, Access1, Refresh1} = logic_auth_session:issue_admin_tokens(AdminId, <<"superadmin">>),
{ok, Access1, Refresh1, Sid1} = logic_auth_session:issue_admin_tokens(AdminId, <<"superadmin">>),
?assert(is_binary(Access1)),
?assert(is_binary(Sid1)),
{ok, _, _} = eventhub_auth:verify_admin_token(Access1),
{ok, Access2, Refresh2} = logic_auth_session:refresh_admin(Refresh1),
{ok, Access2, Refresh2, Sid2} = logic_auth_session:refresh_admin(Refresh1),
?assertEqual(Sid1, Sid2),
?assert(is_binary(Access2)),
?assertNotEqual(Refresh1, Refresh2),
{ok, _, _} = eventhub_auth:verify_admin_token(Access2).
@@ -72,8 +76,8 @@ test_admin_reuse_revokes() ->
created_at = undefined, updated_at = undefined
},
ok = meck:expect(core_admin, get_by_id, fun(_) -> {ok, Admin} end),
{ok, _, Refresh1} = logic_auth_session:issue_admin_tokens(AdminId, <<"admin">>),
{ok, _, Refresh2} = logic_auth_session:refresh_admin(Refresh1),
{ok, _, Refresh1, _} = logic_auth_session:issue_admin_tokens(AdminId, <<"admin">>),
{ok, _, Refresh2, _} = logic_auth_session:refresh_admin(Refresh1),
?assertMatch({error, reuse_detected}, logic_auth_session:refresh_admin(Refresh1)),
?assertMatch({error, revoked}, logic_auth_session:refresh_admin(Refresh2)).
@@ -89,13 +93,14 @@ test_issue_and_refresh_user() ->
ok = meck:expect(core_user, get_by_id, fun(Id) ->
?assertEqual(UserId, Id), {ok, User}
end),
{ok, Access1, Refresh1} = logic_auth_session:issue_user_tokens(UserId, <<"user">>),
{ok, Access1, Refresh1, Sid1} = logic_auth_session:issue_user_tokens(UserId, <<"user">>),
?assert(is_binary(Access1)),
{ok, _, _} = eventhub_auth:verify_user_token(Access1),
{ok, Claims} = eventhub_auth:verify_user_refresh_token(Refresh1),
?assertEqual(<<"user">>, maps:get(<<"aud">>, Claims)),
?assertEqual(<<"web">>, maps:get(<<"client">>, Claims)),
{ok, Access2, Refresh2} = logic_auth_session:refresh_user(Refresh1),
{ok, Access2, Refresh2, Sid2} = logic_auth_session:refresh_user(Refresh1),
?assertEqual(Sid1, Sid2),
?assert(is_binary(Access2)),
?assertNotEqual(Refresh1, Refresh2),
{ok, _, _} = eventhub_auth:verify_user_token(Access2).
@@ -110,8 +115,8 @@ test_user_reuse_revokes() ->
created_at = undefined, updated_at = undefined
},
ok = meck:expect(core_user, get_by_id, fun(_) -> {ok, User} end),
{ok, _, Refresh1} = logic_auth_session:issue_user_tokens(UserId, <<"user">>),
{ok, _, Refresh2} = logic_auth_session:refresh_user(Refresh1),
{ok, _, Refresh1, _} = logic_auth_session:issue_user_tokens(UserId, <<"user">>),
{ok, _, Refresh2, _} = logic_auth_session:refresh_user(Refresh1),
?assertMatch({error, reuse_detected}, logic_auth_session:refresh_user(Refresh1)),
?assertMatch({error, revoked}, logic_auth_session:refresh_user(Refresh2)).
@@ -125,7 +130,7 @@ test_logout_user() ->
created_at = undefined, updated_at = undefined
},
ok = meck:expect(core_user, get_by_id, fun(_) -> {ok, User} end),
{ok, _, Refresh} = logic_auth_session:issue_user_tokens(UserId, <<"user">>),
{ok, _, Refresh, _} = logic_auth_session:issue_user_tokens(UserId, <<"user">>),
?assertEqual(ok, logic_auth_session:logout_user(Refresh)),
?assertMatch({error, revoked}, logic_auth_session:refresh_user(Refresh)).
@@ -139,6 +144,51 @@ test_logout_admin() ->
created_at = undefined, updated_at = undefined
},
ok = meck:expect(core_admin, get_by_id, fun(_) -> {ok, Admin} end),
{ok, _, Refresh} = logic_auth_session:issue_admin_tokens(AdminId, <<"admin">>),
{ok, _, Refresh, _} = logic_auth_session:issue_admin_tokens(AdminId, <<"admin">>),
?assertEqual(ok, logic_auth_session:logout_admin(Refresh)),
?assertMatch({error, revoked}, logic_auth_session:refresh_admin(Refresh)).
test_mobile_device_and_list() ->
UserId = <<"usr_dev">>,
User = #user{
id = UserId, email = <<"d@test.com">>, role = user,
password_hash = <<>>, status = active, nickname = <<>>,
avatar_url = default, timezone = <<"UTC">>, language = <<"ru">>,
phone = <<>>, preferences = #{}, last_login = undefined,
created_at = undefined, updated_at = undefined
},
ok = meck:expect(core_user, get_by_id, fun(_) -> {ok, User} end),
LongName = list_to_binary(lists:duplicate(200, $x)),
{ok, _, Refresh, Sid} = logic_auth_session:issue_user_tokens(
UserId, <<"user">>, <<"mobile">>, LongName, <<"UA-Test">>),
{ok, Claims} = eventhub_auth:verify_user_refresh_token(Refresh),
?assertEqual(<<"mobile">>, maps:get(<<"client">>, Claims)),
[S] = logic_auth_session:list_user_sessions(UserId),
?assertEqual(Sid, S#auth_session.session_id),
?assertEqual(<<"mobile">>, S#auth_session.client_type),
?assertEqual(<<"UA-Test">>, S#auth_session.user_agent),
?assertEqual(120, byte_size(S#auth_session.device_name)),
?assertMatch({error, invalid_client_type},
logic_auth_session:issue_user_tokens(UserId, <<"user">>, <<"tablet">>, <<>>, <<>>)).
test_revoke_others() ->
UserId = <<"usr_rev">>,
User = #user{
id = UserId, email = <<"r@test.com">>, role = user,
password_hash = <<>>, status = active, nickname = <<>>,
avatar_url = default, timezone = <<"UTC">>, language = <<"ru">>,
phone = <<>>, preferences = #{}, last_login = undefined,
created_at = undefined, updated_at = undefined
},
ok = meck:expect(core_user, get_by_id, fun(_) -> {ok, User} end),
{ok, _, RefreshKeep, KeepId} = logic_auth_session:issue_user_tokens(
UserId, <<"user">>, <<"web">>, <<"Keep">>, <<"ua1">>),
{ok, _, _RefreshOther, OtherId} = logic_auth_session:issue_user_tokens(
UserId, <<"user">>, <<"web">>, <<"Other">>, <<"ua2">>),
{ok, 1} = logic_auth_session:revoke_other_user_sessions(UserId, RefreshKeep),
?assertEqual(ok, logic_auth_session:revoke_user_session(UserId, OtherId)),
Active = logic_auth_session:list_user_sessions(UserId),
?assertEqual(1, length(Active)),
?assertEqual(KeepId, (hd(Active))#auth_session.session_id),
?assertMatch({error, not_found},
logic_auth_session:revoke_user_session(UserId, <<"nope">>)).