/
githubmirror
/
strapi
Обзор
Документация
Войти
/
githubmirror
/
strapi
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
develop
tests/api/core/admin/admin-logout-sessions.test.api.ts
286 строк
11 KB
Ben Irvin
fix(admin): improve SSO session metadata and logout revocation (#26872)
20 июл 2026, 18:02
Не верифицирован
20 июл 2026, 18:02
3b03391
Код
Авторство
О чём код?
'use strict'; import jwt from 'jsonwebtoken'; import { createStrapiInstance, superAdmin } from 'api-tests/strapi'; import { createRequest } from 'api-tests/request'; describe('Admin Logout Sessions', () => { let strapi: any; const cookieName = 'strapi_admin_refresh'; beforeAll(async () => { strapi = await createStrapiInstance({ bootstrap: async ({ strapi: s }: any) => { s.config.set('admin.rateLimit.enabled', false); }, }); }); afterAll(async () => { if (strapi) { await strapi.db.query('admin::session').deleteMany({}); await strapi.destroy(); } }); afterEach(async () => { if (strapi) { await strapi.db.query('admin::session').deleteMany({}); } }); const getCookie = (res: any, name: string): string | undefined => { const setCookies: string[] = res.headers['set-cookie'] || []; return setCookies.find((c) => c.startsWith(`${name}=`)); }; it('logout clears cookie even without refresh cookie present', async () => { // login and capture refresh cookie, then exchange for an access token const rq = createRequest({ strapi }); let loginRes = await rq.post('/admin/login', { body: superAdmin.loginInfo }); expect(loginRes.statusCode).toBe(200); const refreshCookie = getCookie(loginRes, cookieName)!; const cookiePair = refreshCookie.split(';')[0]; const tokenRes = await createRequest({ strapi }).post('/admin/access-token', { headers: { Cookie: cookiePair }, }); expect(tokenRes.statusCode).toBe(200); const accessToken = tokenRes.body?.data?.token as string; // Use only Authorization header (no cookie) for logout const freshRq = createRequest({ strapi }).setToken(accessToken); const res = await freshRq.post('/admin/logout'); expect(res.statusCode).toBe(200); const cookie = getCookie(res, cookieName); expect(cookie).toBeDefined(); // expired cookie is set expect(cookie).toMatch(/expires=/i); }); const deviceId = '22222222-2222-4222-8222-222222222222'; const deviceId2 = '33333333-3333-4333-8333-333333333333'; it('logout with deviceId revokes only that device sessions', async () => { const rq = createRequest({ strapi }); // Login with specific device const body = { ...superAdmin.loginInfo, deviceId }; const loginRes = await rq.post('/admin/login', { body }); expect(loginRes.statusCode).toBe(200); const refreshSetCookie = getCookie(loginRes, cookieName)!; const cookiePair = refreshSetCookie.split(';')[0]; // Create another session with different device const loginRes2 = await rq.post('/admin/login', { body: { ...superAdmin.loginInfo, deviceId: deviceId2 }, }); expect(loginRes2.statusCode).toBe(200); // Logout targeting first device const tokenRes = await createRequest({ strapi }).post('/admin/access-token', { headers: { Cookie: cookiePair }, }); const accessToken = tokenRes.body?.data?.token as string; const res = await createRequest({ strapi }) .setToken(accessToken) .post('/admin/logout', { body: { deviceId } }); expect(res.statusCode).toBe(200); // Verify sessions remaining belong only to the second device const sessions = await strapi.db.query('admin::session').findMany({}); expect(sessions.some((s: any) => s.deviceId === deviceId)).toBe(false); expect(sessions.some((s: any) => s.deviceId === deviceId2)).toBe(true); }); it('logout without deviceId revokes all devices', async () => { const rq = createRequest({ strapi }); await rq.post('/admin/login', { body: { ...superAdmin.loginInfo, deviceId }, }); const loginResB = await rq.post('/admin/login', { body: { ...superAdmin.loginInfo, deviceId: deviceId2 }, }); // Obtain an access token expect(loginResB.statusCode).toBe(200); const maybeCookie = getCookie(loginResB, cookieName); let accessToken: string; const pair = maybeCookie!.split(';')[0]; const tokenRes = await createRequest({ strapi }).post('/admin/access-token', { headers: { Cookie: pair }, }); expect(tokenRes.statusCode).toBe(200); accessToken = tokenRes.body?.data?.token as string; const res = await createRequest({ strapi }).setToken(accessToken).post('/admin/logout'); expect(res.statusCode).toBe(200); // Derive userId from the access token payload const decoded = jwt.verify(accessToken, strapi.config.get('admin.auth.secret')) as { userId: string; }; const userId = String(decoded.userId); const sessions = await strapi.db.query('admin::session').findMany({ where: { userId } }); expect(sessions).toHaveLength(0); }); it('logout revokes the current session when client deviceId differs from the session record (SSO-like)', async () => { const rq = createRequest({ strapi }); const wrongClientDeviceId = '99999999-9999-4999-8999-999999999999'; // SSO omits deviceId, so the server generates one. const loginRes = await rq.post('/admin/login', { body: superAdmin.loginInfo }); expect(loginRes.statusCode).toBe(200); const refreshCookie = getCookie(loginRes, cookieName)!; const cookiePair = refreshCookie.split(';')[0]; const tokenRes = await createRequest({ strapi }).post('/admin/access-token', { headers: { Cookie: cookiePair }, }); const accessToken = tokenRes.body?.data?.token as string; const decoded = jwt.verify(accessToken, strapi.config.get('admin.auth.secret')) as { userId: string; sessionId: string; }; const userId = String(decoded.userId); const sessionFromToken = await strapi.db.query('admin::session').findOne({ where: { sessionId: decoded.sessionId, status: 'active' }, }); expect(sessionFromToken?.deviceId).toBeDefined(); expect(sessionFromToken?.deviceId).not.toBe(wrongClientDeviceId); const res = await createRequest({ strapi }) .setToken(accessToken) .post('/admin/logout', { body: { deviceId: wrongClientDeviceId } }); expect(res.statusCode).toBe(200); const sessionsAfter = await strapi.db.query('admin::session').findMany({ where: { userId, deviceId: sessionFromToken!.deviceId, status: 'active' }, }); expect(sessionsAfter).toHaveLength(0); }); it('SSO-like logout of device A leaves device B active', async () => { const rq = createRequest({ strapi }); const wrongClientDeviceId = 'aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa'; const deviceB = 'bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb'; // Device A: omit deviceId so the server assigns one (SSO-like). const loginA = await rq.post('/admin/login', { body: superAdmin.loginInfo }); expect(loginA.statusCode).toBe(200); const loginB = await rq.post('/admin/login', { body: { ...superAdmin.loginInfo, deviceId: deviceB }, }); expect(loginB.statusCode).toBe(200); const refreshCookieA = getCookie(loginA, cookieName)!; const cookiePairA = refreshCookieA.split(';')[0]; const tokenResA = await createRequest({ strapi }).post('/admin/access-token', { headers: { Cookie: cookiePairA }, }); const accessTokenA = tokenResA.body?.data?.token as string; const decodedA = jwt.verify(accessTokenA, strapi.config.get('admin.auth.secret')) as { userId: string; sessionId: string; }; const userId = String(decodedA.userId); const sessionA = await strapi.db.query('admin::session').findOne({ where: { sessionId: decodedA.sessionId, status: 'active' }, }); expect(sessionA?.deviceId).toBeDefined(); expect(sessionA?.deviceId).not.toBe(wrongClientDeviceId); expect(sessionA?.deviceId).not.toBe(deviceB); const logoutA = await createRequest({ strapi }) .setToken(accessTokenA) .post('/admin/logout', { body: { deviceId: wrongClientDeviceId } }); expect(logoutA.statusCode).toBe(200); const sessionsAAfter = await strapi.db.query('admin::session').findMany({ where: { userId, deviceId: sessionA!.deviceId, status: 'active' }, }); expect(sessionsAAfter).toHaveLength(0); const sessionsBAfter = await strapi.db.query('admin::session').findMany({ where: { userId, deviceId: deviceB, status: 'active' }, }); expect(sessionsBAfter.length).toBeGreaterThan(0); const refreshCookieB = getCookie(loginB, cookieName)!; const cookiePairB = refreshCookieB.split(';')[0]; const tokenResB = await createRequest({ strapi }).post('/admin/access-token', { headers: { Cookie: cookiePairB }, }); expect(tokenResB.statusCode).toBe(200); const me = await createRequest({ strapi }) .setToken(tokenResB.body?.data?.token as string) .get('/admin/users/me'); expect(me.statusCode).toBe(200); }); it('device B remains valid after device-scoped logout of device A', async () => { const rq = createRequest({ strapi }); const deviceA = '77777777-7777-4777-8777-777777777777'; const deviceB = '88888888-8888-4888-8888-888888888888'; // Create A and B sessions const loginA = await rq.post('/admin/login', { body: { ...superAdmin.loginInfo, deviceId: deviceA }, }); expect(loginA.statusCode).toBe(200); const loginB = await rq.post('/admin/login', { body: { ...superAdmin.loginInfo, deviceId: deviceB }, }); expect(loginB.statusCode).toBe(200); // Access via device B const refreshCookieB = getCookie(loginB, 'strapi_admin_refresh')!; const cookiePairB = refreshCookieB.split(';')[0]; const accessFromB = await createRequest({ strapi }).post('/admin/access-token', { headers: { Cookie: cookiePairB }, }); expect(accessFromB.statusCode).toBe(200); // Logout device A specifically const refreshCookieA = getCookie(loginA, 'strapi_admin_refresh')!; const cookiePairA = refreshCookieA.split(';')[0]; const accessFromA = await createRequest({ strapi }).post('/admin/access-token', { headers: { Cookie: cookiePairA }, }); const accessTokenA = accessFromA.body?.data?.token as string; const logoutA = await createRequest({ strapi }) .setToken(accessTokenA) .post('/admin/logout', { body: { deviceId: deviceA }, }); expect(logoutA.statusCode).toBe(200); // Device B should still be able to exchange and access protected route const newAccessFromB = await createRequest({ strapi }).post('/admin/access-token', { headers: { Cookie: cookiePairB }, }); expect(newAccessFromB.statusCode).toBe(200); const newAccessTokenB = newAccessFromB.body?.data?.token as string; const me = await createRequest({ strapi }).setToken(newAccessTokenB).get('/admin/users/me'); expect(me.statusCode).toBe(200); }); });