mirror of
https://github.com/strapi/strapi.git
synced 2025-10-15 01:52:45 +00:00
512 lines
19 KiB
JavaScript
512 lines
19 KiB
JavaScript
'use strict';
|
|
|
|
const { createStrapiInstance } = require('api-tests/strapi');
|
|
const { createRequest } = require('api-tests/request');
|
|
const { createAuthenticatedUser } = require('../utils');
|
|
|
|
let strapi;
|
|
|
|
const internals = {
|
|
user: {
|
|
username: 'test-refresh',
|
|
email: 'test-refresh@strapi.io',
|
|
password: 'Test1234',
|
|
confirmed: true,
|
|
provider: 'local',
|
|
},
|
|
};
|
|
|
|
describe('Auth API (refresh mode httpOnly behaviour)', () => {
|
|
beforeAll(async () => {
|
|
strapi = await createStrapiInstance({
|
|
bypassAuth: false,
|
|
async bootstrap({ strapi: s }) {
|
|
s.config.set('plugin::users-permissions.jwtManagement', 'refresh');
|
|
s.config.set('plugin::users-permissions.sessions.httpOnly', false);
|
|
},
|
|
});
|
|
|
|
await createAuthenticatedUser({ strapi, userInfo: internals.user });
|
|
});
|
|
|
|
afterAll(async () => {
|
|
await strapi.db.query('plugin::users-permissions.user').deleteMany();
|
|
await strapi.destroy();
|
|
});
|
|
|
|
test('Default (httpOnly=false): returns jwt and refreshToken in JSON, no cookie set', async () => {
|
|
const rqAuth = createRequest({ strapi }).setURLPrefix('/api/auth');
|
|
|
|
const res = await rqAuth({
|
|
method: 'POST',
|
|
url: '/local',
|
|
body: { identifier: internals.user.email, password: internals.user.password },
|
|
});
|
|
|
|
expect(res.statusCode).toBe(200);
|
|
expect(res.body.jwt).toEqual(expect.any(String));
|
|
expect(res.body.refreshToken).toEqual(expect.any(String));
|
|
const setCookie = res.headers['set-cookie'];
|
|
expect(Array.isArray(setCookie) ? setCookie.join('\n') : String(setCookie || '')).not.toMatch(
|
|
/strapi_up_refresh=/
|
|
);
|
|
});
|
|
|
|
test('Per-request opt-in header sets cookie and omits refreshToken', async () => {
|
|
const rqAuth = createRequest({ strapi }).setURLPrefix('/api/auth');
|
|
|
|
const res = await rqAuth({
|
|
method: 'POST',
|
|
url: '/local',
|
|
headers: { 'x-strapi-refresh-cookie': 'httpOnly' },
|
|
body: { identifier: internals.user.email, password: internals.user.password },
|
|
});
|
|
|
|
expect(res.statusCode).toBe(200);
|
|
expect(res.body.jwt).toEqual(expect.any(String));
|
|
expect(res.body.refreshToken).toBeUndefined();
|
|
const setCookie = res.headers['set-cookie'];
|
|
const cookies = Array.isArray(setCookie) ? setCookie.join('\n') : String(setCookie || '');
|
|
expect(cookies).toMatch(/strapi_up_refresh=/);
|
|
expect(cookies.toLowerCase()).toMatch(/httponly/);
|
|
});
|
|
|
|
test('Config httpOnly=true: always sets cookie and omits refreshToken', async () => {
|
|
await strapi.destroy();
|
|
strapi = await createStrapiInstance({
|
|
bypassAuth: false,
|
|
async bootstrap({ strapi: s }) {
|
|
s.config.set('plugin::users-permissions.jwtManagement', 'refresh');
|
|
s.config.set('plugin::users-permissions.sessions.httpOnly', true);
|
|
},
|
|
});
|
|
|
|
const rqAuth = createRequest({ strapi }).setURLPrefix('/api/auth');
|
|
|
|
// Ensure user exists in this new instance
|
|
await createAuthenticatedUser({ strapi, userInfo: internals.user });
|
|
|
|
const res = await rqAuth({
|
|
method: 'POST',
|
|
url: '/local',
|
|
body: { identifier: internals.user.email, password: internals.user.password },
|
|
});
|
|
|
|
expect(res.statusCode).toBe(200);
|
|
expect(res.body.jwt).toEqual(expect.any(String));
|
|
expect(res.body.refreshToken).toBeUndefined();
|
|
const setCookie = res.headers['set-cookie'];
|
|
const cookies = Array.isArray(setCookie) ? setCookie.join('\n') : String(setCookie || '');
|
|
expect(cookies).toMatch(/strapi_up_refresh=/);
|
|
expect(cookies.toLowerCase()).toMatch(/httponly/);
|
|
});
|
|
|
|
describe('Change Password (refresh mode responses)', () => {
|
|
beforeAll(async () => {
|
|
await strapi.destroy();
|
|
strapi = await createStrapiInstance({
|
|
bypassAuth: false,
|
|
async bootstrap({ strapi: s }) {
|
|
s.config.set('plugin::users-permissions.jwtManagement', 'refresh');
|
|
s.config.set('plugin::users-permissions.sessions.httpOnly', false);
|
|
},
|
|
});
|
|
|
|
// Ensure user exists
|
|
await createAuthenticatedUser({ strapi, userInfo: internals.user });
|
|
});
|
|
test('Fails on unauthenticated request', async () => {
|
|
const nonAuthRequest = createRequest({ strapi });
|
|
|
|
const res = await nonAuthRequest({
|
|
method: 'POST',
|
|
url: '/api/auth/change-password',
|
|
body: {},
|
|
});
|
|
|
|
expect(res.statusCode).toBe(403);
|
|
expect(res.body.error.name).toBe('ForbiddenError');
|
|
expect(res.body.error.message).toBe('Forbidden');
|
|
});
|
|
|
|
test('Returns jwt and refreshToken after successful password change', async () => {
|
|
const rqAuthLogin = createRequest({ strapi }).setURLPrefix('/api/auth');
|
|
|
|
// Create an isolated user for this test to avoid impacting other tests
|
|
const isolatedUser = {
|
|
username: 'test-refresh-change',
|
|
email: 'test-refresh-change@strapi.io',
|
|
password: 'Change1234!',
|
|
confirmed: true,
|
|
provider: 'local',
|
|
};
|
|
await createAuthenticatedUser({ strapi, userInfo: isolatedUser });
|
|
|
|
// Login to obtain a valid jwt to perform change-password
|
|
const loginRes = await rqAuthLogin({
|
|
method: 'POST',
|
|
url: '/local',
|
|
body: { identifier: isolatedUser.email, password: isolatedUser.password },
|
|
});
|
|
|
|
expect(loginRes.statusCode).toBe(200);
|
|
const jwt = loginRes.body.jwt;
|
|
|
|
const rqAuth = createRequest({ strapi }).setURLPrefix('/api/auth').setToken(jwt);
|
|
|
|
const newPassword = 'Change12345!';
|
|
const res = await rqAuth({
|
|
method: 'POST',
|
|
url: '/change-password',
|
|
body: {
|
|
password: newPassword,
|
|
passwordConfirmation: newPassword,
|
|
currentPassword: isolatedUser.password,
|
|
},
|
|
});
|
|
|
|
expect(res.statusCode).toBe(200);
|
|
expect(res.body.jwt).toEqual(expect.any(String));
|
|
expect(res.body.refreshToken).toEqual(expect.any(String));
|
|
|
|
// Can login with the new password afterwards
|
|
const relogin = await rqAuthLogin({
|
|
method: 'POST',
|
|
url: '/local',
|
|
body: { identifier: isolatedUser.email, password: newPassword },
|
|
});
|
|
expect(relogin.statusCode).toBe(200);
|
|
});
|
|
|
|
test('Fails on invalid confirm password', async () => {
|
|
const rqAuthLogin = createRequest({ strapi }).setURLPrefix('/api/auth');
|
|
const loginRes = await rqAuthLogin({
|
|
method: 'POST',
|
|
url: '/local',
|
|
body: { identifier: internals.user.email, password: internals.user.password },
|
|
});
|
|
const jwt = loginRes.body.jwt;
|
|
|
|
const rqAuth = createRequest({ strapi }).setURLPrefix('/api/auth').setToken(jwt);
|
|
const res = await rqAuth({
|
|
method: 'POST',
|
|
url: '/change-password',
|
|
body: {
|
|
password: 'newPassword',
|
|
passwordConfirmation: 'somethingElse',
|
|
currentPassword: 'Test12345!',
|
|
},
|
|
});
|
|
expect(res.statusCode).toBe(400);
|
|
expect(res.body.error.name).toBe('ValidationError');
|
|
expect(res.body.error.message).toBe('Passwords do not match');
|
|
});
|
|
|
|
test('Fails on invalid current password', async () => {
|
|
const rqAuthLogin = createRequest({ strapi }).setURLPrefix('/api/auth');
|
|
const loginRes = await rqAuthLogin({
|
|
method: 'POST',
|
|
url: '/local',
|
|
body: { identifier: internals.user.email, password: internals.user.password },
|
|
});
|
|
const jwt = loginRes.body.jwt;
|
|
|
|
const rqAuth = createRequest({ strapi }).setURLPrefix('/api/auth').setToken(jwt);
|
|
const res = await rqAuth({
|
|
method: 'POST',
|
|
url: '/change-password',
|
|
body: {
|
|
password: 'newPassword',
|
|
passwordConfirmation: 'newPassword',
|
|
currentPassword: 'badPassword',
|
|
},
|
|
});
|
|
expect(res.statusCode).toBe(400);
|
|
expect(res.body.error.name).toBe('ValidationError');
|
|
expect(res.body.error.message).toBe('The provided current password is invalid');
|
|
});
|
|
|
|
test('Fails when current and new password are the same', async () => {
|
|
const rqAuthLogin = createRequest({ strapi }).setURLPrefix('/api/auth');
|
|
const loginRes = await rqAuthLogin({
|
|
method: 'POST',
|
|
url: '/local',
|
|
body: { identifier: internals.user.email, password: internals.user.password },
|
|
});
|
|
const jwt = loginRes.body.jwt;
|
|
|
|
const rqAuth = createRequest({ strapi }).setURLPrefix('/api/auth').setToken(jwt);
|
|
const res = await rqAuth({
|
|
method: 'POST',
|
|
url: '/change-password',
|
|
body: {
|
|
password: internals.user.password,
|
|
passwordConfirmation: internals.user.password,
|
|
currentPassword: internals.user.password,
|
|
},
|
|
});
|
|
expect(res.statusCode).toBe(400);
|
|
expect(res.body.error.name).toBe('ValidationError');
|
|
expect(res.body.error.message).toBe(
|
|
'Your new password must be different than your current password'
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('Refresh endpoint', () => {
|
|
beforeAll(async () => {
|
|
await strapi.destroy();
|
|
strapi = await createStrapiInstance({
|
|
bypassAuth: false,
|
|
async bootstrap({ strapi: s }) {
|
|
s.config.set('plugin::users-permissions.jwtManagement', 'refresh');
|
|
s.config.set('plugin::users-permissions.sessions.httpOnly', false);
|
|
|
|
// Enable public permission for refresh route
|
|
const publicRole = await s.db
|
|
.query('plugin::users-permissions.role')
|
|
.findOne({ where: { type: 'public' } });
|
|
const roleService = s.service('plugin::users-permissions.role');
|
|
const publicRoleDetails = await roleService.findOne(publicRole.id);
|
|
publicRoleDetails.permissions['plugin::users-permissions'] = publicRoleDetails
|
|
.permissions['plugin::users-permissions'] || { controllers: {} };
|
|
const controllers =
|
|
publicRoleDetails.permissions['plugin::users-permissions'].controllers || {};
|
|
const authCtrl = controllers.auth || {};
|
|
authCtrl.refresh = { enabled: true, policy: '' };
|
|
publicRoleDetails.permissions['plugin::users-permissions'].controllers = {
|
|
...controllers,
|
|
auth: authCtrl,
|
|
};
|
|
await roleService.updateRole(publicRole.id, {
|
|
permissions: publicRoleDetails.permissions,
|
|
});
|
|
},
|
|
});
|
|
await createAuthenticatedUser({ strapi, userInfo: internals.user });
|
|
});
|
|
test('Missing refresh token returns 400 (public route)', async () => {
|
|
const rqAuth = createRequest({ strapi }).setURLPrefix('/api/auth');
|
|
|
|
const res = await rqAuth({ method: 'POST', url: '/refresh', body: {} });
|
|
expect(res.statusCode).toBe(400);
|
|
expect(res.body.error.message).toBe('Missing refresh token');
|
|
});
|
|
|
|
test('Invalid refresh token returns 401', async () => {
|
|
const rqAuth = createRequest({ strapi }).setURLPrefix('/api/auth');
|
|
|
|
const res = await rqAuth({
|
|
method: 'POST',
|
|
url: '/refresh',
|
|
body: { refreshToken: 'not-a-valid-token' },
|
|
});
|
|
expect(res.statusCode).toBe(401);
|
|
expect(res.body.error.message).toBe('Invalid refresh token');
|
|
});
|
|
|
|
test('Success returns new jwt and rotated refreshToken in JSON by default', async () => {
|
|
const rqAuth = createRequest({ strapi }).setURLPrefix('/api/auth');
|
|
|
|
// Login to get a refresh token (default httpOnly=false)
|
|
const loginRes = await rqAuth({
|
|
method: 'POST',
|
|
url: '/local',
|
|
body: { identifier: internals.user.email, password: internals.user.password },
|
|
});
|
|
expect(loginRes.statusCode).toBe(200);
|
|
const initialRefresh = loginRes.body.refreshToken;
|
|
expect(initialRefresh).toEqual(expect.any(String));
|
|
|
|
const refreshRes = await rqAuth({
|
|
method: 'POST',
|
|
url: '/refresh',
|
|
body: { refreshToken: initialRefresh },
|
|
});
|
|
expect(refreshRes.statusCode).toBe(200);
|
|
expect(refreshRes.body.jwt).toEqual(expect.any(String));
|
|
expect(refreshRes.body.refreshToken).toEqual(expect.any(String));
|
|
expect(refreshRes.body.refreshToken).not.toBe(initialRefresh);
|
|
});
|
|
|
|
test('With httpOnly header, sets cookie and omits refreshToken in body', async () => {
|
|
const rqAuth = createRequest({ strapi }).setURLPrefix('/api/auth');
|
|
|
|
// Login to get a refresh token first
|
|
const loginRes = await rqAuth({
|
|
method: 'POST',
|
|
url: '/local',
|
|
body: { identifier: internals.user.email, password: internals.user.password },
|
|
});
|
|
const initialRefresh = loginRes.body.refreshToken;
|
|
|
|
const res = await rqAuth({
|
|
method: 'POST',
|
|
url: '/refresh',
|
|
headers: { 'x-strapi-refresh-cookie': 'httpOnly' },
|
|
body: { refreshToken: initialRefresh },
|
|
});
|
|
|
|
expect(res.statusCode).toBe(200);
|
|
expect(res.body.jwt).toEqual(expect.any(String));
|
|
expect(res.body.refreshToken).toBeUndefined();
|
|
const setCookie = res.headers['set-cookie'];
|
|
const cookies = Array.isArray(setCookie) ? setCookie.join('\n') : String(setCookie || '');
|
|
expect(cookies).toMatch(/strapi_up_refresh=/);
|
|
expect(cookies.toLowerCase()).toMatch(/httponly/);
|
|
});
|
|
|
|
test('Even with cookie set, missing body yields 400', async () => {
|
|
const rqAuth = createRequest({ strapi }).setURLPrefix('/api/auth');
|
|
|
|
const loginRes = await rqAuth({
|
|
method: 'POST',
|
|
url: '/local',
|
|
headers: { 'x-strapi-refresh-cookie': 'httpOnly' },
|
|
body: { identifier: internals.user.email, password: 'Test12345!' },
|
|
});
|
|
const setCookie = loginRes.headers['set-cookie'];
|
|
const cookieHeader = Array.isArray(setCookie) ? setCookie : [setCookie];
|
|
|
|
const res = await rqAuth({
|
|
method: 'POST',
|
|
url: '/refresh',
|
|
headers: {
|
|
Cookie: cookieHeader.filter(Boolean).join('; '),
|
|
'x-strapi-refresh-cookie': 'httpOnly',
|
|
},
|
|
body: {},
|
|
});
|
|
expect(res.statusCode).toBe(400);
|
|
expect(res.body.error.message).toBe('Missing refresh token');
|
|
});
|
|
});
|
|
|
|
describe('Logout endpoint', () => {
|
|
beforeAll(async () => {
|
|
await strapi.destroy();
|
|
strapi = await createStrapiInstance({
|
|
bypassAuth: false,
|
|
async bootstrap({ strapi: s }) {
|
|
s.config.set('plugin::users-permissions.jwtManagement', 'refresh');
|
|
s.config.set('plugin::users-permissions.sessions.httpOnly', false);
|
|
// Disable rate limiting for auth routes in this suite to avoid 429s
|
|
s.config.set('plugin::users-permissions.ratelimit', { enabled: false });
|
|
|
|
// Enable authenticated permission for logout and public for refresh (used later)
|
|
const roleService = s.service('plugin::users-permissions.role');
|
|
const publicRole = await s.db
|
|
.query('plugin::users-permissions.role')
|
|
.findOne({ where: { type: 'public' } });
|
|
const authenticatedRole = await s.db
|
|
.query('plugin::users-permissions.role')
|
|
.findOne({ where: { type: 'authenticated' } });
|
|
|
|
const publicRoleDetails = await roleService.findOne(publicRole.id);
|
|
publicRoleDetails.permissions['plugin::users-permissions'] = publicRoleDetails
|
|
.permissions['plugin::users-permissions'] || { controllers: {} };
|
|
const pubControllers =
|
|
publicRoleDetails.permissions['plugin::users-permissions'].controllers || {};
|
|
const pubAuthCtrl = pubControllers.auth || {};
|
|
pubAuthCtrl.refresh = { enabled: true, policy: '' };
|
|
publicRoleDetails.permissions['plugin::users-permissions'].controllers = {
|
|
...pubControllers,
|
|
auth: pubAuthCtrl,
|
|
};
|
|
await roleService.updateRole(publicRole.id, {
|
|
permissions: publicRoleDetails.permissions,
|
|
});
|
|
|
|
const authRoleDetails = await roleService.findOne(authenticatedRole.id);
|
|
authRoleDetails.permissions['plugin::users-permissions'] = authRoleDetails.permissions[
|
|
'plugin::users-permissions'
|
|
] || { controllers: {} };
|
|
const authControllers =
|
|
authRoleDetails.permissions['plugin::users-permissions'].controllers || {};
|
|
const authAuthCtrl = authControllers.auth || {};
|
|
authAuthCtrl.logout = { enabled: true, policy: '' };
|
|
authRoleDetails.permissions['plugin::users-permissions'].controllers = {
|
|
...authControllers,
|
|
auth: authAuthCtrl,
|
|
};
|
|
await roleService.updateRole(authenticatedRole.id, {
|
|
permissions: authRoleDetails.permissions,
|
|
});
|
|
},
|
|
});
|
|
await createAuthenticatedUser({ strapi, userInfo: internals.user });
|
|
});
|
|
test('Requires authentication', async () => {
|
|
const rqAuth = createRequest({ strapi }).setURLPrefix('/api/auth');
|
|
const res = await rqAuth({ method: 'POST', url: '/logout', body: {} });
|
|
expect([401, 403]).toContain(res.statusCode);
|
|
if (res.statusCode === 401) {
|
|
expect(res.body.error.message).toBe('Missing authentication');
|
|
}
|
|
});
|
|
|
|
test('Authenticated logout responds ok and clears cookie when requested', async () => {
|
|
const rqAuth = createRequest({ strapi }).setURLPrefix('/api/auth');
|
|
|
|
// Login to get access jwt
|
|
const loginRes = await rqAuth({
|
|
method: 'POST',
|
|
url: '/local',
|
|
body: { identifier: internals.user.email, password: internals.user.password },
|
|
});
|
|
expect(loginRes.statusCode).toBe(200);
|
|
const jwt = loginRes.body.jwt;
|
|
expect(typeof jwt).toBe('string');
|
|
|
|
const rqAuthed = createRequest({ strapi }).setURLPrefix('/api/auth');
|
|
|
|
const res = await rqAuthed({
|
|
method: 'POST',
|
|
url: '/logout',
|
|
headers: { 'x-strapi-refresh-cookie': 'httpOnly', Authorization: `Bearer ${jwt}` },
|
|
body: {},
|
|
});
|
|
expect(res.statusCode).toBe(200);
|
|
expect(res.body).toMatchObject({ ok: true });
|
|
const setCookie = res.headers['set-cookie'];
|
|
const cookies = Array.isArray(setCookie) ? setCookie.join('\n') : String(setCookie || '');
|
|
expect(cookies).toMatch(/strapi_up_refresh=;/);
|
|
});
|
|
|
|
test('Logout invalidates refresh token', async () => {
|
|
const rqAuth = createRequest({ strapi }).setURLPrefix('/api/auth');
|
|
|
|
// Login to get jwt and refresh token
|
|
const loginRes = await rqAuth({
|
|
method: 'POST',
|
|
url: '/local',
|
|
body: { identifier: internals.user.email, password: internals.user.password },
|
|
});
|
|
expect(loginRes.statusCode).toBe(200);
|
|
const jwt = loginRes.body.jwt;
|
|
const refreshToken = loginRes.body.refreshToken;
|
|
expect(typeof jwt).toBe('string');
|
|
expect(typeof refreshToken).toBe('string');
|
|
|
|
// Logout
|
|
const rqAuthed = createRequest({ strapi }).setURLPrefix('/api/auth');
|
|
const logoutRes = await rqAuthed({
|
|
method: 'POST',
|
|
url: '/logout',
|
|
headers: { Authorization: `Bearer ${jwt}` },
|
|
body: {},
|
|
});
|
|
expect(logoutRes.statusCode).toBe(200);
|
|
|
|
// Attempt to refresh with the old token should fail
|
|
const refreshRes = await rqAuth({
|
|
method: 'POST',
|
|
url: '/refresh',
|
|
body: { refreshToken },
|
|
});
|
|
expect(refreshRes.statusCode).toBe(401);
|
|
expect(refreshRes.body.error.message).toBe('Invalid refresh token');
|
|
});
|
|
});
|
|
});
|