diff --git a/meet-ce/backend/tests/integration/api/api-keys/create-api-key.test.ts b/meet-ce/backend/tests/integration/api/api-keys/create-api-key.test.ts index 5c28a74f..0fa45660 100644 --- a/meet-ce/backend/tests/integration/api/api-keys/create-api-key.test.ts +++ b/meet-ce/backend/tests/integration/api/api-keys/create-api-key.test.ts @@ -18,8 +18,7 @@ describe('API Keys API Tests', () => { beforeAll(async () => { app = await startTestServer(); - const { accessToken } = await loginRootAdmin(); - rootAdminAccessToken = accessToken; + ({ accessToken: rootAdminAccessToken } = await loginRootAdmin()); }); afterAll(async () => { diff --git a/meet-ce/backend/tests/integration/api/auth/logout.test.ts b/meet-ce/backend/tests/integration/api/auth/logout.test.ts index 0f2049ea..90488987 100644 --- a/meet-ce/backend/tests/integration/api/auth/logout.test.ts +++ b/meet-ce/backend/tests/integration/api/auth/logout.test.ts @@ -12,8 +12,7 @@ describe('Authentication API Tests', () => { beforeAll(async () => { app = await startTestServer(); - const { accessToken } = await loginRootAdmin(); - rootAdminAccessToken = accessToken; + ({ accessToken: rootAdminAccessToken } = await loginRootAdmin()); }); describe('Logout Tests', () => { diff --git a/meet-ce/backend/tests/integration/api/auth/refresh-token.test.ts b/meet-ce/backend/tests/integration/api/auth/refresh-token.test.ts index ded4dfaf..87ab8262 100644 --- a/meet-ce/backend/tests/integration/api/auth/refresh-token.test.ts +++ b/meet-ce/backend/tests/integration/api/auth/refresh-token.test.ts @@ -72,7 +72,7 @@ describe('Authentication API Tests', () => { expect(response.body.message).toContain('Invalid refresh token'); }); - it('should fail when using roomMemberToken token instead of refresh token', async () => { + it('should fail when using room member token instead of refresh token', async () => { const { moderatorToken } = await setupSingleRoom(); const response = await request(app) .post(`${AUTH_PATH}/refresh`) diff --git a/meet-ce/backend/tests/integration/api/auth/token-validation.test.ts b/meet-ce/backend/tests/integration/api/auth/token-validation.test.ts new file mode 100644 index 00000000..7769e5d7 --- /dev/null +++ b/meet-ce/backend/tests/integration/api/auth/token-validation.test.ts @@ -0,0 +1,357 @@ +import { beforeAll, describe, expect, it } from '@jest/globals'; +import { MeetUserRole } from '@openvidu-meet/typings'; +import { Express } from 'express'; +import request from 'supertest'; +import { INTERNAL_CONFIG } from '../../../../src/config/internal-config.js'; +import { + createUser, + deleteAllRooms, + deleteAllUsers, + deleteUser, + loginUser, + resetUserPassword, + sleep, + startTestServer +} from '../../../helpers/request-helpers.js'; +import { setupSingleRoom, setupTestUsers, setupUser } from '../../../helpers/test-scenarios.js'; +import { TestUsers, UserData } from '../../../interfaces/scenarios.js'; + +const USERS_PATH = `${INTERNAL_CONFIG.INTERNAL_API_BASE_PATH_V1}/users`; +const ROOMS_PATH = `${INTERNAL_CONFIG.API_BASE_PATH_V1}/rooms`; + +describe('Token Validation Tests', () => { + let app: Express; + let testUsers: TestUsers; + + let roomId: string; + let roomMemberToken: string; + + beforeAll(async () => { + app = await startTestServer(); + testUsers = await setupTestUsers(); + + // Setup a room with a member token + const roomData = await setupSingleRoom(); + roomId = roomData.room.roomId; + roomMemberToken = roomData.moderatorToken; + }); + + afterAll(async () => { + await deleteAllRooms(); + await deleteAllUsers(); + }); + + describe('Access Token Tests', () => { + it('should succeed when providing valid access token', async () => { + const response = await request(app) + .get(`${USERS_PATH}/me`) + .set(INTERNAL_CONFIG.ACCESS_TOKEN_HEADER, testUsers.user.accessToken); + expect(response.status).toBe(200); + }); + + it('should succeed when providing valid access token through query param without bearer prefix', async () => { + const accessTokenQuery = testUsers.user.accessToken.replace('Bearer ', ''); + const response = await request(app).get(`${USERS_PATH}/me`).query({ accessToken: accessTokenQuery }); + expect(response.status).toBe(200); + }); + + it('should succeed when admin accesses admin-only endpoint', async () => { + const response = await request(app) + .get(`${INTERNAL_CONFIG.INTERNAL_API_BASE_PATH_V1}/api-keys`) + .set(INTERNAL_CONFIG.ACCESS_TOKEN_HEADER, testUsers.admin.accessToken); + expect(response.status).toBe(200); + }); + + it('should fail when access token is missing', async () => { + const response = await request(app).get(`${USERS_PATH}/me`); + expect(response.status).toBe(401); + expect(response.body).toHaveProperty('message'); + expect(response.body.message).toContain('Unauthorized'); + }); + + it('should fail when access token is invalid', async () => { + const response = await request(app) + .get(`${USERS_PATH}/me`) + .set(INTERNAL_CONFIG.ACCESS_TOKEN_HEADER, 'Bearer invalidtoken'); + expect(response.status).toBe(401); + expect(response.body).toHaveProperty('message'); + expect(response.body.message).toContain('Invalid token'); + }); + + it('should fail when access token is expired', async () => { + // Set short access token expiration + const initialTokenExpiration = INTERNAL_CONFIG.ACCESS_TOKEN_EXPIRATION; + INTERNAL_CONFIG.ACCESS_TOKEN_EXPIRATION = '1s'; + + // Create a user and get their access token + const userData = await setupUser({ + userId: `user_${Date.now()}`, + name: 'User', + password: 'password123', + role: MeetUserRole.USER + }); + + await sleep('2s'); // Ensure the token is expired + + // Restore original expiration after setup + INTERNAL_CONFIG.ACCESS_TOKEN_EXPIRATION = initialTokenExpiration; + + const response = await request(app) + .get(`${USERS_PATH}/me`) + .set(INTERNAL_CONFIG.ACCESS_TOKEN_HEADER, userData.accessToken); + expect(response.status).toBe(401); + expect(response.body).toHaveProperty('message'); + expect(response.body.message).toContain('Invalid token'); + }); + + it('should fail when using room member token instead of access token', async () => { + const response = await request(app) + .get(`${USERS_PATH}/me`) + .set(INTERNAL_CONFIG.ACCESS_TOKEN_HEADER, roomMemberToken); + expect(response.status).toBe(401); + expect(response.body).toHaveProperty('message'); + expect(response.body.message).toContain('Invalid token'); + }); + + it('should fail when using refresh token instead of access token', async () => { + const response = await request(app) + .get(`${USERS_PATH}/me`) + .set(INTERNAL_CONFIG.ACCESS_TOKEN_HEADER, testUsers.user.refreshToken); + expect(response.status).toBe(401); + expect(response.body).toHaveProperty('message'); + }); + + it('should fail when token subject (user) does not exist', async () => { + // Create a user and get their access token + const userData = await setupUser({ + userId: `user_${Date.now()}`, + name: 'User', + password: 'password123', + role: MeetUserRole.USER + }); + + // Delete the user to invalidate the token subject + await deleteUser(userData.user.userId); + + // Attempt to use the token + const response = await request(app) + .get(`${USERS_PATH}/me`) + .set(INTERNAL_CONFIG.ACCESS_TOKEN_HEADER, userData.accessToken); + expect(response.status).toBe(403); + expect(response.body).toHaveProperty('message'); + expect(response.body.message).toContain('Invalid token subject'); + }); + + it('should fail when USER tries to access ADMIN-only endpoint', async () => { + const response = await request(app) + .get(`${INTERNAL_CONFIG.INTERNAL_API_BASE_PATH_V1}/api-keys`) + .set(INTERNAL_CONFIG.ACCESS_TOKEN_HEADER, testUsers.user.accessToken); + expect(response.status).toBe(403); + expect(response.body).toHaveProperty('message'); + expect(response.body.message).toContain('Insufficient permissions'); + }); + + it('should fail when ROOM_MEMBER tries to access ADMIN and USER endpoint', async () => { + const response = await request(app) + .get(USERS_PATH) + .set(INTERNAL_CONFIG.ACCESS_TOKEN_HEADER, testUsers.roomMember.accessToken); + expect(response.status).toBe(403); + expect(response.body).toHaveProperty('message'); + expect(response.body.message).toContain('Insufficient permissions'); + }); + + describe('Password Change Required Tests', () => { + const resetPassword = 'NewPassword123'; + let userData: UserData; + + beforeAll(async () => { + // Create a user and get their access token + userData = await setupUser({ + userId: `user_${Date.now()}`, + name: 'User', + password: 'password123', + role: MeetUserRole.USER + }); + + // Reset user password to force password change + await resetUserPassword(userData.user.userId, resetPassword); + }); + + it('should succeed when accessing /me endpoint with mustChangePassword', async () => { + const response = await request(app) + .get(`${USERS_PATH}/me`) + .set(INTERNAL_CONFIG.ACCESS_TOKEN_HEADER, userData.accessToken); + expect(response.status).toBe(200); + }); + + it('should succeed when accessing /change-password endpoint with mustChangePassword', async () => { + const response = await request(app) + .post(`${USERS_PATH}/change-password`) + .set(INTERNAL_CONFIG.ACCESS_TOKEN_HEADER, userData.accessToken) + .send({ + currentPassword: resetPassword, + newPassword: userData.password + }); + expect(response.status).toBe(200); + + // Reset user password again for futher tests + await resetUserPassword(userData.user.userId, resetPassword); + }); + + it('should fail when accessing other endpoints with mustChangePassword', async () => { + // Try to create a room (requires USER role which this user has, but password change blocks it) + const response = await request(app) + .post(ROOMS_PATH) + .set(INTERNAL_CONFIG.ACCESS_TOKEN_HEADER, userData.accessToken) + .send({}); + expect(response.status).toBe(403); + expect(response.body).toHaveProperty('message'); + expect(response.body.message).toContain('Password change required'); + }); + }); + + describe('Temporary Access Token Tests', () => { + let userId: string; + let accessTokenTmp: string; + + beforeAll(async () => { + // Create a user (when created, this user is set to require password change) + const response = await createUser({ + userId: 'temp_user', + name: 'Temp User', + password: 'InitialPassword1!', + role: MeetUserRole.USER + }); + userId = response.body.userId; + + // Login to get temporary access token + ({ accessToken: accessTokenTmp } = await loginUser(userId, 'InitialPassword1!')); + }); + + it('should succeed when accessing /me endpoint with temporary token', async () => { + const response = await request(app) + .get(`${USERS_PATH}/me`) + .set(INTERNAL_CONFIG.ACCESS_TOKEN_HEADER, accessTokenTmp); + expect(response.status).toBe(200); + }); + + it('should succeed when accessing /change-password endpoint with temporary token', async () => { + const response = await request(app) + .post(`${USERS_PATH}/change-password`) + .set(INTERNAL_CONFIG.ACCESS_TOKEN_HEADER, accessTokenTmp) + .send({ + currentPassword: 'InitialPassword1!', + newPassword: 'FinalPassword123!' + }); + expect(response.status).toBe(200); + }); + + it('should fail when accessing other endpoints with temporary token', async () => { + const response = await request(app) + .post(ROOMS_PATH) + .set(INTERNAL_CONFIG.ACCESS_TOKEN_HEADER, accessTokenTmp) + .send({}); + expect(response.status).toBe(403); + expect(response.body).toHaveProperty('message'); + expect(response.body.message).toContain('Password change required'); + }); + }); + }); + + describe('Room Member Token Tests', () => { + it('should succeed when providing valid room member token', async () => { + const response = await request(app) + .get(`${ROOMS_PATH}/${roomId}`) + .set(INTERNAL_CONFIG.ROOM_MEMBER_TOKEN_HEADER, roomMemberToken); + expect(response.status).toBe(200); + }); + + it('should succeed when providing valid room member token through query param without bearer prefix', async () => { + const roomMemberTokenQuery = roomMemberToken.replace('Bearer ', ''); + const response = await request(app) + .get(`${ROOMS_PATH}/${roomId}`) + .query({ roomMemberToken: roomMemberTokenQuery }); + expect(response.status).toBe(200); + }); + + it('should fail when room member token is missing', async () => { + const response = await request(app).get(`${ROOMS_PATH}/${roomId}`); + expect(response.status).toBe(401); + expect(response.body).toHaveProperty('message'); + expect(response.body.message).toContain('Unauthorized'); + }); + + it('should fail when room member token is invalid', async () => { + const response = await request(app) + .get(`${ROOMS_PATH}/${roomId}`) + .set(INTERNAL_CONFIG.ROOM_MEMBER_TOKEN_HEADER, 'Bearer invalidtoken'); + expect(response.status).toBe(401); + expect(response.body).toHaveProperty('message'); + expect(response.body.message).toContain('Invalid token'); + }); + + it('should fail when room member token is expired', async () => { + // Set short room member token expiration + const initialTokenExpiration = INTERNAL_CONFIG.ROOM_MEMBER_TOKEN_EXPIRATION; + INTERNAL_CONFIG.ROOM_MEMBER_TOKEN_EXPIRATION = '1s'; + + const roomData = await setupSingleRoom(); + await sleep('2s'); // Ensure the token is expired + + // Restore original expiration after setup + INTERNAL_CONFIG.ROOM_MEMBER_TOKEN_EXPIRATION = initialTokenExpiration; + + const response = await request(app) + .get(`${ROOMS_PATH}/${roomData.room.roomId}`) + .set(INTERNAL_CONFIG.ROOM_MEMBER_TOKEN_HEADER, roomData.moderatorToken); + expect(response.status).toBe(401); + expect(response.body).toHaveProperty('message'); + expect(response.body.message).toContain('Invalid token'); + }); + + it('should fail when using access token instead of room member token', async () => { + const response = await request(app) + .get(`${ROOMS_PATH}/${roomId}`) + .set(INTERNAL_CONFIG.ROOM_MEMBER_TOKEN_HEADER, testUsers.user.accessToken); + expect(response.status).toBe(401); + expect(response.body).toHaveProperty('message'); + expect(response.body.message).toContain('Invalid token'); + }); + + it('should succeed when both room member token and user access token are valid', async () => { + const response = await request(app) + .get(`${ROOMS_PATH}/${roomId}`) + .set(INTERNAL_CONFIG.ROOM_MEMBER_TOKEN_HEADER, roomMemberToken) + .set(INTERNAL_CONFIG.ACCESS_TOKEN_HEADER, testUsers.user.accessToken); + expect(response.status).toBe(200); + }); + + it('should succeed with room member token even when access token is invalid', async () => { + const response = await request(app) + .get(`${ROOMS_PATH}/${roomId}`) + .set(INTERNAL_CONFIG.ROOM_MEMBER_TOKEN_HEADER, roomMemberToken) + .set(INTERNAL_CONFIG.ACCESS_TOKEN_HEADER, 'Bearer invalidtoken'); + expect(response.status).toBe(200); + }); + + it('should fail when room member token is expired, even if user access token is valid', async () => { + // Set short room member token expiration + const initialTokenExpiration = INTERNAL_CONFIG.ROOM_MEMBER_TOKEN_EXPIRATION; + INTERNAL_CONFIG.ROOM_MEMBER_TOKEN_EXPIRATION = '1s'; + + const roomData = await setupSingleRoom(); + await sleep('2s'); // Ensure the token is expired + + // Restore original expiration after setup + INTERNAL_CONFIG.ROOM_MEMBER_TOKEN_EXPIRATION = initialTokenExpiration; + + const response = await request(app) + .get(`${ROOMS_PATH}/${roomData.room.roomId}`) + .set(INTERNAL_CONFIG.ACCESS_TOKEN_HEADER, testUsers.admin.accessToken) + .set(INTERNAL_CONFIG.ROOM_MEMBER_TOKEN_HEADER, roomData.moderatorToken); + expect(response.status).toBe(401); + expect(response.body).toHaveProperty('message'); + expect(response.body.message).toContain('Invalid token'); + }); + }); +}); diff --git a/meet-ce/backend/tests/integration/api/security/meeting-security.test.ts b/meet-ce/backend/tests/integration/api/security/meeting-security.test.ts index 3c26e646..b8a591a0 100644 --- a/meet-ce/backend/tests/integration/api/security/meeting-security.test.ts +++ b/meet-ce/backend/tests/integration/api/security/meeting-security.test.ts @@ -30,8 +30,7 @@ describe('Meeting API Security Tests', () => { beforeAll(async () => { app = await startTestServer(); - const { accessToken } = await loginRootAdmin(); - rootAdminAccessToken = accessToken; + ({ accessToken: rootAdminAccessToken } = await loginRootAdmin()); roomData = await setupSingleRoom(); roomId = roomData.room.roomId; diff --git a/meet-ce/backend/tests/integration/api/security/room-security.test.ts b/meet-ce/backend/tests/integration/api/security/room-security.test.ts index e5903cbd..bd1cb45d 100644 --- a/meet-ce/backend/tests/integration/api/security/room-security.test.ts +++ b/meet-ce/backend/tests/integration/api/security/room-security.test.ts @@ -3,7 +3,7 @@ import { Express } from 'express'; import request from 'supertest'; import { INTERNAL_CONFIG } from '../../../../src/config/internal-config.js'; import { MEET_ENV } from '../../../../src/environment.js'; -import { deleteAllRooms, deleteAllUsers, sleep, startTestServer } from '../../../helpers/request-helpers.js'; +import { deleteAllRooms, deleteAllUsers, startTestServer } from '../../../helpers/request-helpers.js'; import { setupSingleRoom, setupTestUsers, setupTestUsersForRoom } from '../../../helpers/test-scenarios.js'; import { RoomData, RoomTestUsers, TestUsers } from '../../../interfaces/scenarios.js'; @@ -325,24 +325,6 @@ describe('Room API Security Tests', () => { .set(INTERNAL_CONFIG.ROOM_MEMBER_TOKEN_HEADER, newRoomData.moderatorToken); expect(response.status).toBe(403); }); - - it('should fail when room member token is expired, even if user access token is valid', async () => { - // Set short room member token expiration - const initialTokenExpiration = INTERNAL_CONFIG.ROOM_MEMBER_TOKEN_EXPIRATION; - INTERNAL_CONFIG.ROOM_MEMBER_TOKEN_EXPIRATION = '1s'; - - const newRoomData = await setupSingleRoom(); - await sleep('2s'); // Ensure the token is expired - - // Restore original expiration after setup - INTERNAL_CONFIG.ROOM_MEMBER_TOKEN_EXPIRATION = initialTokenExpiration; - - const response = await request(app) - .get(`${ROOMS_PATH}/${newRoomData.room.roomId}`) - .set(INTERNAL_CONFIG.ACCESS_TOKEN_HEADER, testUsers.admin.accessToken) - .set(INTERNAL_CONFIG.ROOM_MEMBER_TOKEN_HEADER, newRoomData.moderatorToken); - expect(response.status).toBe(401); - }); }); describe('Delete Room Tests', () => { diff --git a/meet-ce/backend/tests/integration/api/security/user-security.test.ts b/meet-ce/backend/tests/integration/api/security/user-security.test.ts index f8978332..6e517548 100644 --- a/meet-ce/backend/tests/integration/api/security/user-security.test.ts +++ b/meet-ce/backend/tests/integration/api/security/user-security.test.ts @@ -23,8 +23,7 @@ describe('User API Security Tests', () => { beforeAll(async () => { app = await startTestServer(); - const { accessToken } = await loginRootAdmin(); - rootAdminAccessToken = accessToken; + ({ accessToken: rootAdminAccessToken } = await loginRootAdmin()); testUsers = await setupTestUsers(); });