diff --git a/backend/src/config/internal-config.ts b/backend/src/config/internal-config.ts index 6d1e0a5..f67652a 100644 --- a/backend/src/config/internal-config.ts +++ b/backend/src/config/internal-config.ts @@ -11,6 +11,14 @@ const INTERNAL_CONFIG = { PARTICIPANT_TOKEN_COOKIE_NAME: 'OvMeetParticipantToken', RECORDING_TOKEN_COOKIE_NAME: 'OvMeetRecordingToken', + // Headers names + API_KEY_HEADER: 'x-api-key', + ACCESS_TOKEN_HEADER: 'authorization', + REFRESH_TOKEN_HEADER: 'x-refresh-token', + PARTICIPANT_TOKEN_HEADER: 'x-participant-token', + PARTICIPANT_ROLE_HEADER: 'x-participant-role', + RECORDING_TOKEN_HEADER: 'x-recording-token', + // Token expiration times ACCESS_TOKEN_EXPIRATION: '2h', REFRESH_TOKEN_EXPIRATION: '1d', @@ -21,10 +29,6 @@ const INTERNAL_CONFIG = { PARTICIPANT_MAX_CONCURRENT_NAME_REQUESTS: '20', // Maximum number of request by the same name at the same time allowed PARTICIPANT_NAME_RESERVATION_TTL: '12h' as StringValue, // Time-to-live for participant name reservations - // Headers for API requests - API_KEY_HEADER: 'x-api-key', - PARTICIPANT_ROLE_HEADER: 'x-participant-role', - // Authentication usernames ANONYMOUS_USER: 'anonymous', API_USER: 'api-user', diff --git a/backend/src/controllers/auth.controller.ts b/backend/src/controllers/auth.controller.ts index 5a58fca..97d3998 100644 --- a/backend/src/controllers/auth.controller.ts +++ b/backend/src/controllers/auth.controller.ts @@ -1,3 +1,4 @@ +import { AuthTransportMode } from '@typings-ce'; import { Request, Response } from 'express'; import { ClaimGrants } from 'livekit-server-sdk'; import { container } from '../config/index.js'; @@ -11,7 +12,7 @@ import { rejectRequestFromMeetError } from '../models/error.model.js'; import { AuthService, LoggerService, TokenService, UserService } from '../services/index.js'; -import { getCookieOptions } from '../utils/index.js'; +import { getAuthTransportMode, getCookieOptions, getRefreshToken } from '../utils/index.js'; export const login = async (req: Request, res: Response) => { const logger = container.get(LoggerService); @@ -31,38 +32,60 @@ export const login = async (req: Request, res: Response) => { const tokenService = container.get(TokenService); const accessToken = await tokenService.generateAccessToken(user); const refreshToken = await tokenService.generateRefreshToken(user); - res.cookie( - INTERNAL_CONFIG.ACCESS_TOKEN_COOKIE_NAME, - accessToken, - getCookieOptions('/', INTERNAL_CONFIG.ACCESS_TOKEN_EXPIRATION) - ); - res.cookie( - INTERNAL_CONFIG.REFRESH_TOKEN_COOKIE_NAME, - refreshToken, - getCookieOptions( - `${INTERNAL_CONFIG.INTERNAL_API_BASE_PATH_V1}/auth`, - INTERNAL_CONFIG.REFRESH_TOKEN_EXPIRATION - ) - ); + logger.info(`Login succeeded for user '${username}'`); - return res.status(200).json({ message: `User '${username}' logged in successfully` }); + const transportMode = await getAuthTransportMode(); + + if (transportMode === AuthTransportMode.HEADER) { + // Send tokens in response body for header mode + return res.status(200).json({ + message: `User '${username}' logged in successfully`, + accessToken, + refreshToken + }); + } else { + // Send tokens as cookies for cookie mode + res.cookie( + INTERNAL_CONFIG.ACCESS_TOKEN_COOKIE_NAME, + accessToken, + getCookieOptions('/', INTERNAL_CONFIG.ACCESS_TOKEN_EXPIRATION) + ); + res.cookie( + INTERNAL_CONFIG.REFRESH_TOKEN_COOKIE_NAME, + refreshToken, + getCookieOptions( + `${INTERNAL_CONFIG.INTERNAL_API_BASE_PATH_V1}/auth`, + INTERNAL_CONFIG.REFRESH_TOKEN_EXPIRATION + ) + ); + return res.status(200).json({ message: `User '${username}' logged in successfully` }); + } } catch (error) { - handleError(res, error, 'generating token'); + handleError(res, error, 'generating access and refresh tokens'); } }; -export const logout = (_req: Request, res: Response) => { - res.clearCookie(INTERNAL_CONFIG.ACCESS_TOKEN_COOKIE_NAME); - res.clearCookie(INTERNAL_CONFIG.REFRESH_TOKEN_COOKIE_NAME, { - path: `${INTERNAL_CONFIG.INTERNAL_API_BASE_PATH_V1}/auth` - }); +export const logout = async (_req: Request, res: Response) => { + const transportMode = await getAuthTransportMode(); + + if (transportMode === AuthTransportMode.COOKIE) { + // Clear cookies only in cookie mode + res.clearCookie(INTERNAL_CONFIG.ACCESS_TOKEN_COOKIE_NAME); + res.clearCookie(INTERNAL_CONFIG.REFRESH_TOKEN_COOKIE_NAME, { + path: `${INTERNAL_CONFIG.INTERNAL_API_BASE_PATH_V1}/auth` + }); + } + + // In header mode, the client is responsible for clearing localStorage return res.status(200).json({ message: 'Logout successful' }); }; export const refreshToken = async (req: Request, res: Response) => { const logger = container.get(LoggerService); logger.verbose('Refresh token request received'); - const refreshToken = req.cookies[INTERNAL_CONFIG.REFRESH_TOKEN_COOKIE_NAME]; + + // Get refresh token from cookie or header based on transport mode + const refreshToken = await getRefreshToken(req); if (!refreshToken) { logger.warn('No refresh token provided'); @@ -93,13 +116,25 @@ export const refreshToken = async (req: Request, res: Response) => { try { const accessToken = await tokenService.generateAccessToken(user); - res.cookie( - INTERNAL_CONFIG.ACCESS_TOKEN_COOKIE_NAME, - accessToken, - getCookieOptions('/', INTERNAL_CONFIG.ACCESS_TOKEN_EXPIRATION) - ); + logger.info(`Access token refreshed for user '${username}'`); - return res.status(200).json({ message: `Access token for user '${username}' successfully refreshed` }); + const transportMode = await getAuthTransportMode(); + + if (transportMode === AuthTransportMode.HEADER) { + // Send access token in response body for header mode + return res.status(200).json({ + message: `Access token for user '${username}' successfully refreshed`, + accessToken + }); + } else { + // Send access token as cookie for cookie mode + res.cookie( + INTERNAL_CONFIG.ACCESS_TOKEN_COOKIE_NAME, + accessToken, + getCookieOptions('/', INTERNAL_CONFIG.ACCESS_TOKEN_EXPIRATION) + ); + return res.status(200).json({ message: `Access token for user '${username}' successfully refreshed` }); + } } catch (error) { handleError(res, error, 'refreshing token'); } diff --git a/backend/src/controllers/global-config/security-config.controller.ts b/backend/src/controllers/global-config/security-config.controller.ts index 7f24d0b..d1ed996 100644 --- a/backend/src/controllers/global-config/security-config.controller.ts +++ b/backend/src/controllers/global-config/security-config.controller.ts @@ -13,11 +13,7 @@ export const updateSecurityConfig = async (req: Request, res: Response) => { try { const globalConfig = await storageService.getGlobalConfig(); - const currentAuth = globalConfig.securityConfig.authentication; - const newAuth = securityConfig.authentication; - - currentAuth.authMethod = newAuth.authMethod; - currentAuth.authModeToAccessRoom = newAuth.authModeToAccessRoom; + globalConfig.securityConfig.authentication = { ...securityConfig.authentication }; await storageService.saveGlobalConfig(globalConfig); return res.status(200).json({ message: 'Security config updated successfully' }); diff --git a/backend/src/controllers/participant.controller.ts b/backend/src/controllers/participant.controller.ts index dd3a63f..533484b 100644 --- a/backend/src/controllers/participant.controller.ts +++ b/backend/src/controllers/participant.controller.ts @@ -1,4 +1,4 @@ -import { OpenViduMeetPermissions, ParticipantOptions, ParticipantRole } from '@typings-ce'; +import { AuthTransportMode, OpenViduMeetPermissions, ParticipantOptions, ParticipantRole } from '@typings-ce'; import { Request, Response } from 'express'; import { container } from '../config/index.js'; import INTERNAL_CONFIG from '../config/internal-config.js'; @@ -9,23 +9,25 @@ import { rejectRequestFromMeetError } from '../models/error.model.js'; import { LoggerService, ParticipantService, RoomService, TokenService } from '../services/index.js'; -import { getCookieOptions } from '../utils/index.js'; +import { getAuthTransportMode, getCookieOptions, getRecordingToken } from '../utils/index.js'; export const generateParticipantToken = async (req: Request, res: Response) => { const logger = container.get(LoggerService); const participantService = container.get(ParticipantService); + const tokenService = container.get(TokenService); + const participantOptions: ParticipantOptions = req.body; const { roomId } = participantOptions; - // Check if there is a previous token + // Check if there is a previous token (only for cookie mode) const previousToken = req.cookies[INTERNAL_CONFIG.PARTICIPANT_TOKEN_COOKIE_NAME]; let currentRoles: { role: ParticipantRole; permissions: OpenViduMeetPermissions }[] = []; if (previousToken) { // If there is a previous token, extract the roles from it // and use them to generate the new token, aggregating the new role to the current ones + // This logic is only used in cookie mode to allow multiple roles across tabs logger.verbose('Previous participant token found. Extracting roles'); - const tokenService = container.get(TokenService); try { const claims = tokenService.getClaimsIgnoringExpiration(previousToken); @@ -38,9 +40,15 @@ export const generateParticipantToken = async (req: Request, res: Response) => { try { logger.verbose(`Generating participant token for room '${roomId}'`); - const token = await participantService.generateOrRefreshParticipantToken(participantOptions, currentRoles); - res.cookie(INTERNAL_CONFIG.PARTICIPANT_TOKEN_COOKIE_NAME, token, getCookieOptions('/')); + + const authTransportMode = await getAuthTransportMode(); + + // Send participant token as cookie for cookie mode + if (authTransportMode === AuthTransportMode.COOKIE) { + res.cookie(INTERNAL_CONFIG.PARTICIPANT_TOKEN_COOKIE_NAME, token, getCookieOptions('/')); + } + return res.status(200).json({ token }); } catch (error) { handleError(res, error, `generating participant token for room '${roomId}'`); @@ -49,9 +57,11 @@ export const generateParticipantToken = async (req: Request, res: Response) => { export const refreshParticipantToken = async (req: Request, res: Response) => { const logger = container.get(LoggerService); + const tokenService = container.get(TokenService); + const participantService = container.get(ParticipantService); // Check if there is a previous token - const previousToken = req.cookies[INTERNAL_CONFIG.PARTICIPANT_TOKEN_COOKIE_NAME]; + const previousToken = await getRecordingToken(req); if (!previousToken) { logger.verbose('No previous participant token found. Cannot refresh.'); @@ -60,8 +70,6 @@ export const refreshParticipantToken = async (req: Request, res: Response) => { } // Extract roles from the previous token - const tokenService = container.get(TokenService); - const participantService = container.get(ParticipantService); let currentRoles: { role: ParticipantRole; permissions: OpenViduMeetPermissions }[] = []; try { @@ -85,7 +93,13 @@ export const refreshParticipantToken = async (req: Request, res: Response) => { true ); - res.cookie(INTERNAL_CONFIG.PARTICIPANT_TOKEN_COOKIE_NAME, token, getCookieOptions('/')); + const authTransportMode = await getAuthTransportMode(); + + // Send participant token as cookie for cookie mode + if (authTransportMode === AuthTransportMode.COOKIE) { + res.cookie(INTERNAL_CONFIG.PARTICIPANT_TOKEN_COOKIE_NAME, token, getCookieOptions('/')); + } + return res.status(200).json({ token }); } catch (error) { handleError(res, error, `refreshing participant token for room '${roomId}'`); diff --git a/backend/src/controllers/room.controller.ts b/backend/src/controllers/room.controller.ts index 817874d..6238c92 100644 --- a/backend/src/controllers/room.controller.ts +++ b/backend/src/controllers/room.controller.ts @@ -1,4 +1,5 @@ import { + AuthTransportMode, MeetRoomDeletionPolicyWithMeeting, MeetRoomDeletionPolicyWithRecordings, MeetRoomDeletionSuccessCode, @@ -12,7 +13,7 @@ import { container } from '../config/index.js'; import INTERNAL_CONFIG from '../config/internal-config.js'; import { handleError } from '../models/error.model.js'; import { LoggerService, ParticipantService, RoomService } from '../services/index.js'; -import { getBaseUrl, getCookieOptions } from '../utils/index.js'; +import { getAuthTransportMode, getBaseUrl, getCookieOptions } from '../utils/index.js'; export const createRoom = async (req: Request, res: Response) => { const logger = container.get(LoggerService); @@ -192,12 +193,17 @@ export const generateRecordingToken = async (req: Request, res: Response) => { try { const token = await roomService.generateRecordingToken(roomId, secret); + const authTransportMode = await getAuthTransportMode(); + + // Send recording token as cookie for cookie mode + if (authTransportMode === AuthTransportMode.COOKIE) { + res.cookie( + INTERNAL_CONFIG.RECORDING_TOKEN_COOKIE_NAME, + token, + getCookieOptions('/', INTERNAL_CONFIG.RECORDING_TOKEN_EXPIRATION) + ); + } - res.cookie( - INTERNAL_CONFIG.RECORDING_TOKEN_COOKIE_NAME, - token, - getCookieOptions('/', INTERNAL_CONFIG.RECORDING_TOKEN_EXPIRATION) - ); return res.status(200).json({ token }); } catch (error) { handleError(res, error, `generating recording token for room '${roomId}'`); diff --git a/backend/src/middlewares/auth.middleware.ts b/backend/src/middlewares/auth.middleware.ts index bf2a13e..6e8e16d 100644 --- a/backend/src/middlewares/auth.middleware.ts +++ b/backend/src/middlewares/auth.middleware.ts @@ -26,6 +26,7 @@ import { TokenService, UserService } from '../services/index.js'; +import { getAccessToken, getParticipantToken, getRecordingToken } from '../utils/index.js'; /** * This middleware allows to chain multiple validators to check if the request is authorized. @@ -68,7 +69,7 @@ export const withAuth = (...validators: ((req: Request) => Promise)[]): Re // Configure token validatior for role-based access export const tokenAndRoleValidator = (role: UserRole) => { return async (req: Request) => { - const token = req.cookies[INTERNAL_CONFIG.ACCESS_TOKEN_COOKIE_NAME]; + const token = await getAccessToken(req); if (!token) { throw errorWithControl(errorUnauthorized(), false); @@ -102,7 +103,8 @@ export const tokenAndRoleValidator = (role: UserRole) => { // Configure token validator for participant access export const participantTokenValidator = async (req: Request) => { - await validateTokenAndSetSession(req, INTERNAL_CONFIG.PARTICIPANT_TOKEN_COOKIE_NAME); + const token = await getParticipantToken(req); + await validateTokenAndSetSession(req, token); // Check if the participant role is provided in the request headers // This is required to distinguish roles when multiple are present in the token @@ -140,7 +142,8 @@ export const participantTokenValidator = async (req: Request) => { // Configure token validator for recording access export const recordingTokenValidator = async (req: Request) => { - await validateTokenAndSetSession(req, INTERNAL_CONFIG.RECORDING_TOKEN_COOKIE_NAME); + const token = await getRecordingToken(req); + await validateTokenAndSetSession(req, token); // Validate the recording token metadata try { @@ -153,9 +156,7 @@ export const recordingTokenValidator = async (req: Request) => { } }; -const validateTokenAndSetSession = async (req: Request, cookieName: string) => { - const token = req.cookies[cookieName]; - +const validateTokenAndSetSession = async (req: Request, token: string | undefined) => { if (!token) { throw errorWithControl(errorUnauthorized(), false); } @@ -221,7 +222,7 @@ const getAuthenticatedUserOrAnonymous = async (req: Request): Promise => { let user: User | null = null; // Check if there is a user already authenticated - const token = req.cookies[INTERNAL_CONFIG.ACCESS_TOKEN_COOKIE_NAME]; + const token = await getAccessToken(req); if (token) { try { diff --git a/backend/src/middlewares/request-validators/config-validator.middleware.ts b/backend/src/middlewares/request-validators/config-validator.middleware.ts index 627cce9..0f911bc 100644 --- a/backend/src/middlewares/request-validators/config-validator.middleware.ts +++ b/backend/src/middlewares/request-validators/config-validator.middleware.ts @@ -1,6 +1,7 @@ import { AuthenticationConfig, AuthMode, + AuthTransportMode, AuthType, SecurityConfig, SingleUserAuth, @@ -39,6 +40,8 @@ const WebhookTestSchema = z.object({ .regex(/^https?:\/\//, { message: 'URL must start with http:// or https://' }) }); +const AuthTransportModeSchema = z.enum([AuthTransportMode.COOKIE, AuthTransportMode.HEADER]); + const AuthModeSchema: z.ZodType = z.enum([AuthMode.NONE, AuthMode.MODERATORS_ONLY, AuthMode.ALL_USERS]); const AuthTypeSchema: z.ZodType = z.enum([AuthType.SINGLE_USER]); @@ -51,6 +54,7 @@ const ValidAuthMethodSchema: z.ZodType = SingleUserAuthSchema; const AuthenticationConfigSchema: z.ZodType = z.object({ authMethod: ValidAuthMethodSchema, + authTransportMode: AuthTransportModeSchema, authModeToAccessRoom: AuthModeSchema }); diff --git a/backend/src/services/storage/storage.service.ts b/backend/src/services/storage/storage.service.ts index 3cb1a35..81c0ee4 100644 --- a/backend/src/services/storage/storage.service.ts +++ b/backend/src/services/storage/storage.service.ts @@ -1,4 +1,14 @@ -import { AuthMode, AuthType, GlobalConfig, MeetApiKey, MeetRecordingInfo, MeetRoom, User, UserRole } from '@typings-ce'; +import { + AuthMode, + AuthTransportMode, + AuthType, + GlobalConfig, + MeetApiKey, + MeetRecordingInfo, + MeetRoom, + User, + UserRole +} from '@typings-ce'; import { inject, injectable } from 'inversify'; import ms from 'ms'; import { Readable } from 'stream'; @@ -722,7 +732,8 @@ export class MeetStorageService< authMethod: { type: AuthType.SINGLE_USER }, - authModeToAccessRoom: AuthMode.NONE + authModeToAccessRoom: AuthMode.NONE, + authTransportMode: AuthTransportMode.COOKIE } }, roomsConfig: { diff --git a/backend/src/utils/index.ts b/backend/src/utils/index.ts index fb9bdd0..2122e47 100644 --- a/backend/src/utils/index.ts +++ b/backend/src/utils/index.ts @@ -1,3 +1,4 @@ export * from './array.utils.js'; export * from './cookie.utils.js'; +export * from './token.utils.js'; export * from './url.utils.js'; diff --git a/backend/src/utils/token.utils.ts b/backend/src/utils/token.utils.ts new file mode 100644 index 0000000..70677e3 --- /dev/null +++ b/backend/src/utils/token.utils.ts @@ -0,0 +1,121 @@ +import { AuthTransportMode } from '@typings-ce'; +import { Request } from 'express'; +import { container } from '../config/index.js'; +import INTERNAL_CONFIG from '../config/internal-config.js'; +import { LoggerService, MeetStorageService } from '../services/index.js'; + +/** + * Gets the current authentication transport mode from global config. + * + * @returns The current transport mode + */ +export const getAuthTransportMode = async (): Promise => { + try { + const storageService = container.get(MeetStorageService); + const globalConfig = await storageService.getGlobalConfig(); + return globalConfig.securityConfig.authentication.authTransportMode; + } catch (error) { + const logger = container.get(LoggerService); + logger.error('Error fetching auth transport mode:', error); + // Fallback to cookie mode in case of error + return AuthTransportMode.COOKIE; + } +}; + +/** + * Extracts the access token from the request based on the configured transport mode. + * + * @param req - Express request object + * @returns The JWT token string or undefined if not found + */ +export const getAccessToken = async (req: Request): Promise => { + return getTokenFromRequest( + req, + INTERNAL_CONFIG.ACCESS_TOKEN_HEADER, + INTERNAL_CONFIG.ACCESS_TOKEN_COOKIE_NAME, + 'accessToken' + ); +}; + +/** + * Extracts the refresh token from the request based on the configured transport mode. + * + * @param req - Express request object + * @returns The JWT refresh token string or undefined if not found + */ +export const getRefreshToken = async (req: Request): Promise => { + return getTokenFromRequest(req, INTERNAL_CONFIG.REFRESH_TOKEN_HEADER, INTERNAL_CONFIG.REFRESH_TOKEN_COOKIE_NAME); +}; + +/** + * Extracts the participant token from the request based on the configured transport mode. + * + * @param req - Express request object + * @returns The JWT participant token string or undefined if not found + */ +export const getParticipantToken = async (req: Request): Promise => { + return getTokenFromRequest( + req, + INTERNAL_CONFIG.PARTICIPANT_TOKEN_HEADER, + INTERNAL_CONFIG.PARTICIPANT_TOKEN_COOKIE_NAME + ); +}; + +/** + * Extracts the recording token from the request based on the configured transport mode. + * + * @param req - Express request object + * @returns The JWT recording token string or undefined if not found + */ +export const getRecordingToken = async (req: Request): Promise => { + return getTokenFromRequest( + req, + INTERNAL_CONFIG.RECORDING_TOKEN_HEADER, + INTERNAL_CONFIG.RECORDING_TOKEN_COOKIE_NAME, + 'recordingToken' + ); +}; + +/** + * Generic function to extract a JWT token from the request based on the configured transport mode. + * + * @param req - Express request object + * @param headerName - Name of the header to check + * @param cookieName - Name of the cookie to check + * @param queryParamName - (Optional) Name of the query parameter to check (for access and recording tokens) + * @returns The JWT token string or undefined if not found + */ +const getTokenFromRequest = async ( + req: Request, + headerName: string, + cookieName: string, + queryParamName?: string +): Promise => { + const transportMode = await getAuthTransportMode(); + + if (transportMode === AuthTransportMode.COOKIE) { + // Try to get from cookie + return req.cookies[cookieName]; + } + + // Try to get from header + const headerValue = req.headers[headerName]; + + // Header value must be a string starting with 'Bearer ' + if (headerValue && typeof headerValue === 'string' && headerValue.startsWith('Bearer ')) { + return headerValue.substring(7); + } + + /** + * If not found in header, try to get from query parameter + * This is needed to send access/recording tokens via URL for video playback + * since we cannot set custom headers in video element requests + */ + if (queryParamName) { + const token = req.query[queryParamName]; + + if (token && typeof token === 'string') { + return token; + } + } +}; diff --git a/frontend/projects/shared-meet-components/src/lib/guards/extract-query-params.guard.ts b/frontend/projects/shared-meet-components/src/lib/guards/extract-query-params.guard.ts index f33485c..90e8fe1 100644 --- a/frontend/projects/shared-meet-components/src/lib/guards/extract-query-params.guard.ts +++ b/frontend/projects/shared-meet-components/src/lib/guards/extract-query-params.guard.ts @@ -17,7 +17,7 @@ export const extractRoomQueryParamsGuard: CanActivateFn = (route: ActivatedRoute const sessionStorageService = inject(SessionStorageService); const { roomId, secret: querySecret, participantName, leaveRedirectUrl, showOnlyRecordings } = extractParams(route); - const secret = querySecret || sessionStorageService.getRoomSecret(roomId); + const secret = querySecret || sessionStorageService.getRoomSecret(); // Handle leave redirect URL logic handleLeaveRedirectUrl(leaveRedirectUrl); @@ -48,7 +48,7 @@ export const extractRecordingQueryParamsGuard: CanActivateFn = (route: Activated const sessionStorageService = inject(SessionStorageService); const { roomId, secret: querySecret } = extractParams(route); - const secret = querySecret || sessionStorageService.getRoomSecret(roomId); + const secret = querySecret || sessionStorageService.getRoomSecret(); if (!secret) { // If no secret is provided, redirect to the error page @@ -83,9 +83,8 @@ const handleLeaveRedirectUrl = (leaveRedirectUrl: string | undefined) => { return; } - // Absolute path provided in embedded mode - construct full URL + // Absolute path provided in embedded mode - construct full URL based on parent origin if (isEmbeddedMode && leaveRedirectUrl?.startsWith('/')) { - // If in embedded mode and a absolute path is provided, construct full URL based on parent origin const parentUrl = document.referrer; const parentOrigin = new URL(parentUrl).origin; navigationService.setLeaveRedirectUrl(parentOrigin + leaveRedirectUrl); diff --git a/frontend/projects/shared-meet-components/src/lib/interceptors/http.interceptor.ts b/frontend/projects/shared-meet-components/src/lib/interceptors/http.interceptor.ts index 58e1ef9..1468082 100644 --- a/frontend/projects/shared-meet-components/src/lib/interceptors/http.interceptor.ts +++ b/frontend/projects/shared-meet-components/src/lib/interceptors/http.interceptor.ts @@ -1,28 +1,68 @@ import { HttpErrorResponse, HttpEvent, HttpHandlerFn, HttpInterceptorFn, HttpRequest } from '@angular/common/http'; import { inject } from '@angular/core'; import { Router } from '@angular/router'; -import { AuthService, ParticipantService, RecordingService, RoomService } from '@lib/services'; +import { AuthService, ParticipantService, RecordingService, RoomService, TokenStorageService } from '@lib/services'; import { catchError, from, Observable, switchMap } from 'rxjs'; +/** + * Adds all necessary authorization headers to the request based on available tokens + * - authorization: Bearer token for access token (from localStorage) + * - x-participant-token: Bearer token for participant token (from sessionStorage) + * - x-recording-token: Bearer token for recording token (from sessionStorage) + */ +const addAuthHeadersIfNeeded = ( + req: HttpRequest, + tokenStorageService: TokenStorageService +): HttpRequest => { + const headers: { [key: string]: string } = {}; + + // Add access token header if available + const accessToken = tokenStorageService.getAccessToken(); + if (accessToken) { + headers['authorization'] = `Bearer ${accessToken}`; + } + + // Add participant token header if available + const participantToken = tokenStorageService.getParticipantToken(); + if (participantToken) { + headers['x-participant-token'] = `Bearer ${participantToken}`; + } + + // Add recording token header if available + const recordingToken = tokenStorageService.getRecordingToken(); + if (recordingToken) { + headers['x-recording-token'] = `Bearer ${recordingToken}`; + } + + // Clone request with all headers at once if any were added + return Object.keys(headers).length > 0 ? req.clone({ setHeaders: headers }) : req; +}; + export const httpInterceptor: HttpInterceptorFn = (req: HttpRequest, next: HttpHandlerFn) => { const router: Router = inject(Router); const authService: AuthService = inject(AuthService); const roomService = inject(RoomService); const participantTokenService = inject(ParticipantService); const recordingService = inject(RecordingService); + const tokenStorageService = inject(TokenStorageService); const pageUrl = router.currentNavigation()?.finalUrl?.toString() || router.url; const requestUrl = req.url; + // Clone request with credentials for cookie mode req = req.clone({ withCredentials: true }); + // Add all authorization headers if tokens exist + req = addAuthHeadersIfNeeded(req, tokenStorageService); + const refreshAccessToken = (firstError: HttpErrorResponse) => { console.log('Refreshing access token...'); - return authService.refreshToken().pipe( + return from(authService.refreshToken()).pipe( switchMap(() => { console.log('Access token refreshed'); + req = addAuthHeadersIfNeeded(req, tokenStorageService); return next(req); }), catchError(async (error: HttpErrorResponse) => { @@ -55,6 +95,7 @@ export const httpInterceptor: HttpInterceptorFn = (req: HttpRequest, ne ).pipe( switchMap(() => { console.log('Participant token refreshed'); + req = addAuthHeadersIfNeeded(req, tokenStorageService); return next(req); }), catchError((error: HttpErrorResponse) => { @@ -76,6 +117,7 @@ export const httpInterceptor: HttpInterceptorFn = (req: HttpRequest, ne return from(recordingService.generateRecordingToken(roomId, secret)).pipe( switchMap(() => { console.log('Recording token refreshed'); + req = addAuthHeadersIfNeeded(req, tokenStorageService); return next(req); }), catchError((error: HttpErrorResponse) => { diff --git a/frontend/projects/shared-meet-components/src/lib/pages/console/config/config.component.ts b/frontend/projects/shared-meet-components/src/lib/pages/console/config/config.component.ts index 8d9609a..313217b 100644 --- a/frontend/projects/shared-meet-components/src/lib/pages/console/config/config.component.ts +++ b/frontend/projects/shared-meet-components/src/lib/pages/console/config/config.component.ts @@ -243,9 +243,7 @@ export class ConfigComponent implements OnInit { if (!this.initialFormValue) return; this.handleThemeChange(); - this.updateColorChangesState(); - this.updateFormChangeState(); } @@ -259,9 +257,9 @@ export class ConfigComponent implements OnInit { * Handles theme change by updating non-customized colors */ private handleThemeChange(): void { - const currentFormValue = this.appearanceForm.value; - const newBaseTheme = currentFormValue.baseTheme || MeetRoomThemeMode.LIGHT; + const newBaseTheme = this.appearanceForm.value.baseTheme || MeetRoomThemeMode.LIGHT; if (newBaseTheme === this.lastBaseThemeValue) return; + const newDefaults = this.defaultColors[newBaseTheme]; // Build update object with only non-customized colors @@ -276,6 +274,7 @@ export class ConfigComponent implements OnInit { if (Object.keys(updatedColors).length === 0) { return; } + // Apply updates atomically this.applyColorUpdates(updatedColors, newBaseTheme); } @@ -294,11 +293,8 @@ export class ConfigComponent implements OnInit { * Updates color changes state efficiently */ private updateColorChangesState(): void { - const currentFormValue = this.appearanceForm.value; const colorKeys: ColorField[] = this.colorFields.map((field) => field.key); - - const hasColorChanges = colorKeys.some((key) => currentFormValue[key] !== this.initialFormValue![key]); - + const hasColorChanges = colorKeys.some((key) => this.appearanceForm.value[key] !== this.initialFormValue![key]); this.hasColorChanges.set(hasColorChanges); } diff --git a/frontend/projects/shared-meet-components/src/lib/pages/meeting/meeting.component.ts b/frontend/projects/shared-meet-components/src/lib/pages/meeting/meeting.component.ts index 3f6cea5..5e18b99 100644 --- a/frontend/projects/shared-meet-components/src/lib/pages/meeting/meeting.component.ts +++ b/frontend/projects/shared-meet-components/src/lib/pages/meeting/meeting.component.ts @@ -28,6 +28,7 @@ import { RecordingService, RoomService, SessionStorageService, + TokenStorageService, WebComponentManagerService } from '@lib/services'; import { @@ -64,27 +65,27 @@ import { import { combineLatest, Subject, takeUntil } from 'rxjs'; @Component({ - selector: 'ov-meeting', - templateUrl: './meeting.component.html', - styleUrls: ['./meeting.component.scss'], - imports: [ - OpenViduComponentsUiModule, - // ApiDirectiveModule, - CommonModule, - MatFormFieldModule, - MatInputModule, - FormsModule, - ReactiveFormsModule, - MatCardModule, - MatButtonModule, - MatIconModule, - MatIconButton, - MatMenuModule, - MatDividerModule, - MatTooltipModule, - MatRippleModule, - ShareMeetingLinkComponent - ] + selector: 'ov-meeting', + templateUrl: './meeting.component.html', + styleUrls: ['./meeting.component.scss'], + imports: [ + OpenViduComponentsUiModule, + // ApiDirectiveModule, + CommonModule, + MatFormFieldModule, + MatInputModule, + FormsModule, + ReactiveFormsModule, + MatCardModule, + MatButtonModule, + MatIconModule, + MatIconButton, + MatMenuModule, + MatDividerModule, + MatTooltipModule, + MatRippleModule, + ShareMeetingLinkComponent + ] }) export class MeetingComponent implements OnInit { participantForm = new FormGroup({ @@ -130,7 +131,8 @@ export class MeetingComponent implements OnInit { protected clipboard: Clipboard, protected viewportService: ViewportService, protected ovThemeService: OpenViduThemeService, - protected configService: GlobalConfigService + protected configService: GlobalConfigService, + protected tokenStorageService: TokenStorageService ) { this.features = this.featureConfService.features; @@ -488,9 +490,12 @@ export class MeetingComponent implements OnInit { }; this.wcManagerService.sendMessageToParent(message); - // Remove the moderator secret from session storage if the participant left for a reason other than browser unload + // Remove the moderator secret (and stored tokens) from session storage + // if the participant left for a reason other than browser unload if (event.reason !== ParticipantLeftReason.BROWSER_UNLOAD) { - this.sessionStorageService.removeRoomSecret(event.roomName); + this.sessionStorageService.removeRoomSecret(); + this.tokenStorageService.clearParticipantToken(); + this.tokenStorageService.clearRecordingToken(); } // Navigate to the disconnected page with the reason diff --git a/frontend/projects/shared-meet-components/src/lib/pages/view-recording/view-recording.component.ts b/frontend/projects/shared-meet-components/src/lib/pages/view-recording/view-recording.component.ts index 3458ded..e0e781b 100644 --- a/frontend/projects/shared-meet-components/src/lib/pages/view-recording/view-recording.component.ts +++ b/frontend/projects/shared-meet-components/src/lib/pages/view-recording/view-recording.component.ts @@ -14,19 +14,19 @@ import { formatDurationToTime } from '@lib/utils'; import { ViewportService } from 'openvidu-components-angular'; @Component({ - selector: 'ov-view-recording', - templateUrl: './view-recording.component.html', - styleUrls: ['./view-recording.component.scss'], - imports: [ - MatCardModule, - MatButtonModule, - MatIconModule, - DatePipe, - MatProgressSpinnerModule, - MatTooltipModule, - MatSnackBarModule, - RecordingVideoPlayerComponent - ] + selector: 'ov-view-recording', + templateUrl: './view-recording.component.html', + styleUrls: ['./view-recording.component.scss'], + imports: [ + MatCardModule, + MatButtonModule, + MatIconModule, + DatePipe, + MatProgressSpinnerModule, + MatTooltipModule, + MatSnackBarModule, + RecordingVideoPlayerComponent + ] }) export class ViewRecordingComponent implements OnInit { recording?: MeetRecordingInfo; @@ -138,6 +138,4 @@ export class ViewRecordingComponent implements OnInit { this.router.navigate(['/recordings']); } } - - } diff --git a/frontend/projects/shared-meet-components/src/lib/services/auth.service.ts b/frontend/projects/shared-meet-components/src/lib/services/auth.service.ts index 4cd5369..4665a75 100644 --- a/frontend/projects/shared-meet-components/src/lib/services/auth.service.ts +++ b/frontend/projects/shared-meet-components/src/lib/services/auth.service.ts @@ -1,6 +1,6 @@ import { HttpErrorResponse } from '@angular/common/http'; import { Injectable } from '@angular/core'; -import { HttpService, NavigationService } from '@lib/services'; +import { HttpService, NavigationService, TokenStorageService } from '@lib/services'; import { MeetApiKey, User, UserRole } from '@lib/typings/ce'; import { from, Observable } from 'rxjs'; @@ -16,6 +16,7 @@ export class AuthService { constructor( protected httpService: HttpService, + protected tokenStorageService: TokenStorageService, protected navigationService: NavigationService ) {} @@ -23,7 +24,14 @@ export class AuthService { try { const path = `${this.AUTH_API}/login`; const body = { username, password }; - await this.httpService.postRequest(path, body); + const response = await this.httpService.postRequest(path, body); + + // Check if we got tokens in the response (header mode) + if (response.accessToken && response.refreshToken) { + this.tokenStorageService.setAccessToken(response.accessToken); + this.tokenStorageService.setRefreshToken(response.refreshToken); + } + await this.getAuthenticatedUser(true); } catch (err) { const error = err as HttpErrorResponse; @@ -32,10 +40,25 @@ export class AuthService { } } - refreshToken(): Observable { + async refreshToken() { const path = `${this.AUTH_API}/refresh`; - const response = this.httpService.postRequest(path); - return from(response); + const refreshToken = this.tokenStorageService.getRefreshToken(); + + // Add refresh token header if in header mode + let headers: Record | undefined; + if (refreshToken) { + headers = {}; + headers['x-refresh-token'] = `Bearer ${refreshToken}`; + } + + const response = await this.httpService.postRequest(path, {}, headers); + + // Update access token in localStorage if returned in response + if (response.accessToken) { + this.tokenStorageService.setAccessToken(response.accessToken); + } + + return response; } async logout(redirectToAfterLogin?: string) { @@ -44,6 +67,9 @@ export class AuthService { await this.httpService.postRequest(path); this.user = null; + // Clear tokens from localStorage if in header mode + this.tokenStorageService.clearAccessAndRefreshTokens(); + // Redirect to login page with a query parameter if provided // to redirect to the original page after login this.navigationService.redirectToLoginPage(redirectToAfterLogin, true); diff --git a/frontend/projects/shared-meet-components/src/lib/services/global-config.service.ts b/frontend/projects/shared-meet-components/src/lib/services/global-config.service.ts index 745ca0f..7ffb17d 100644 --- a/frontend/projects/shared-meet-components/src/lib/services/global-config.service.ts +++ b/frontend/projects/shared-meet-components/src/lib/services/global-config.service.ts @@ -1,6 +1,6 @@ import { Injectable } from '@angular/core'; import { FeatureConfigurationService, HttpService } from '@lib/services'; -import { AuthMode, MeetAppearanceConfig, SecurityConfig, WebhookConfig } from '@lib/typings/ce'; +import { AuthMode, AuthTransportMode, MeetAppearanceConfig, SecurityConfig, WebhookConfig } from '@lib/typings/ce'; import { LoggerService } from 'openvidu-components-angular'; @Injectable({ @@ -41,6 +41,11 @@ export class GlobalConfigService { return this.securityConfig!.authentication.authModeToAccessRoom; } + async getAuthTransportMode(): Promise { + await this.getSecurityConfig(); + return this.securityConfig!.authentication.authTransportMode; + } + async saveSecurityConfig(config: SecurityConfig) { const path = `${this.GLOBAL_CONFIG_API}/security`; await this.httpService.putRequest(path, config); diff --git a/frontend/projects/shared-meet-components/src/lib/services/index.ts b/frontend/projects/shared-meet-components/src/lib/services/index.ts index ff533a2..7fcca19 100644 --- a/frontend/projects/shared-meet-components/src/lib/services/index.ts +++ b/frontend/projects/shared-meet-components/src/lib/services/index.ts @@ -12,4 +12,5 @@ export * from './navigation.service'; export * from './notification.service'; export * from './session-storage.service'; export * from './theme.service'; +export * from './token-storage.service'; export * from './wizard-state.service'; diff --git a/frontend/projects/shared-meet-components/src/lib/services/participant.service.ts b/frontend/projects/shared-meet-components/src/lib/services/participant.service.ts index 2f77231..11b1167 100644 --- a/frontend/projects/shared-meet-components/src/lib/services/participant.service.ts +++ b/frontend/projects/shared-meet-components/src/lib/services/participant.service.ts @@ -1,6 +1,12 @@ import { Injectable } from '@angular/core'; -import { FeatureConfigurationService, HttpService } from '@lib/services'; -import { MeetTokenMetadata, ParticipantOptions, ParticipantPermissions, ParticipantRole } from '@lib/typings/ce'; +import { FeatureConfigurationService, GlobalConfigService, HttpService, TokenStorageService } from '@lib/services'; +import { + AuthTransportMode, + MeetTokenMetadata, + ParticipantOptions, + ParticipantPermissions, + ParticipantRole +} from '@lib/typings/ce'; import { getValidDecodedToken } from '@lib/utils'; import { LoggerService } from 'openvidu-components-angular'; @@ -21,7 +27,9 @@ export class ParticipantService { constructor( protected loggerService: LoggerService, protected httpService: HttpService, - protected featureConfService: FeatureConfigurationService + protected featureConfService: FeatureConfigurationService, + protected globalConfigService: GlobalConfigService, + protected tokenStorageService: TokenStorageService ) { this.log = this.loggerService.get('OpenVidu Meet - ParticipantTokenService'); } @@ -49,6 +57,12 @@ export class ParticipantService { const path = `${this.PARTICIPANTS_API}/token`; const { token } = await this.httpService.postRequest<{ token: string }>(path, participantOptions); + // Store token in sessionStorage for header mode + const authTransportMode = await this.globalConfigService.getAuthTransportMode(); + if (authTransportMode === AuthTransportMode.HEADER) { + this.tokenStorageService.setParticipantToken(token); + } + this.updateParticipantTokenInfo(token); return token; } @@ -63,6 +77,12 @@ export class ParticipantService { const path = `${this.PARTICIPANTS_API}/token/refresh`; const { token } = await this.httpService.postRequest<{ token: string }>(path, participantOptions); + // Store token in sessionStorage for header mode + const authTransportMode = await this.globalConfigService.getAuthTransportMode(); + if (authTransportMode === AuthTransportMode.HEADER) { + this.tokenStorageService.setParticipantToken(token); + } + this.updateParticipantTokenInfo(token); return token; } diff --git a/frontend/projects/shared-meet-components/src/lib/services/recording.service.ts b/frontend/projects/shared-meet-components/src/lib/services/recording.service.ts index 7638a21..c57ab91 100644 --- a/frontend/projects/shared-meet-components/src/lib/services/recording.service.ts +++ b/frontend/projects/shared-meet-components/src/lib/services/recording.service.ts @@ -1,8 +1,15 @@ import { Injectable } from '@angular/core'; import { MatDialog } from '@angular/material/dialog'; import { ShareRecordingDialogComponent } from '@lib/components'; -import { AuthService, FeatureConfigurationService, HttpService, ParticipantService } from '@lib/services'; -import { MeetRecordingFilters, MeetRecordingInfo, RecordingPermissions } from '@lib/typings/ce'; +import { + AuthService, + FeatureConfigurationService, + GlobalConfigService, + HttpService, + ParticipantService, + TokenStorageService +} from '@lib/services'; +import { AuthTransportMode, MeetRecordingFilters, MeetRecordingInfo, RecordingPermissions } from '@lib/typings/ce'; import { getValidDecodedToken } from '@lib/utils'; import { LoggerService } from 'openvidu-components-angular'; @@ -26,6 +33,8 @@ export class RecordingService { protected participantService: ParticipantService, protected authService: AuthService, protected featureConfService: FeatureConfigurationService, + protected globalConfigService: GlobalConfigService, + protected tokenStorageService: TokenStorageService, protected dialog: MatDialog ) { this.log = this.loggerService.get('OpenVidu Meet - RecordingManagerService'); @@ -130,8 +139,21 @@ export class RecordingService { */ getRecordingMediaUrl(recordingId: string, secret?: string): string { const params = new URLSearchParams(); + + // If secret is provided, use it (public/private access mode) if (secret) { params.append('secret', secret); + } else { + // Otherwise, try to use access and/or recording token from sessionStorage (header mode) + const accessToken = this.tokenStorageService.getAccessToken(); + if (accessToken) { + params.append('accessToken', accessToken); + } + + const recordingToken = this.tokenStorageService.getRecordingToken(); + if (recordingToken) { + params.append('recordingToken', recordingToken); + } } const now = Date.now(); @@ -163,6 +185,13 @@ export class RecordingService { try { const { token } = await this.httpService.postRequest<{ token: string }>(path, { secret }); + + // Store token in sessionStorage for header mode + const authTransportMode = await this.globalConfigService.getAuthTransportMode(); + if (authTransportMode === AuthTransportMode.HEADER) { + this.tokenStorageService.setRecordingToken(token); + } + this.setRecordingPermissionsFromToken(token); return this.recordingPermissions; } catch (error) { @@ -244,6 +273,7 @@ export class RecordingService { * Downloads a recording by creating a link and triggering a click event * * @param recording - The recording information containing the ID and filename + * @param secret - Optional secret for accessing the recording */ downloadRecording(recording: MeetRecordingInfo, secret?: string) { const recordingUrl = this.getRecordingMediaUrl(recording.recordingId, secret); @@ -256,14 +286,28 @@ export class RecordingService { /** * Downloads multiple recordings as a ZIP file * - * @param recordings - An array of recording IDs to download + * @param recordingIds - An array of recording IDs to download */ downloadRecordingsAsZip(recordingIds: string[]) { if (recordingIds.length === 0) { throw new Error('No recordings IDs provided for download'); } - const downloadUrl = `${this.RECORDINGS_API}/download?recordingIds=${recordingIds.join(',')}`; + const params = new URLSearchParams(); + params.append('recordingIds', recordingIds.join(',')); + + // Try to add access and/or recording token from sessionStorage (header mode) + const accessToken = this.tokenStorageService.getAccessToken(); + if (accessToken) { + params.append('accessToken', accessToken); + } + + const recordingToken = this.tokenStorageService.getRecordingToken(); + if (recordingToken) { + params.append('recordingToken', recordingToken); + } + + const downloadUrl = `${this.RECORDINGS_API}/download?${params.toString()}`; const link = document.createElement('a'); link.href = downloadUrl; link.download = 'recordings.zip'; diff --git a/frontend/projects/shared-meet-components/src/lib/services/room.service.ts b/frontend/projects/shared-meet-components/src/lib/services/room.service.ts index b4f2d0f..c962583 100644 --- a/frontend/projects/shared-meet-components/src/lib/services/room.service.ts +++ b/frontend/projects/shared-meet-components/src/lib/services/room.service.ts @@ -46,7 +46,7 @@ export class RoomService { setRoomSecret(secret: string, updateStorage = true) { this.roomSecret = secret; if (updateStorage) { - this.sessionStorageService.setRoomSecret(this.roomId, secret); + this.sessionStorageService.setRoomSecret(secret); } } diff --git a/frontend/projects/shared-meet-components/src/lib/services/session-storage.service.ts b/frontend/projects/shared-meet-components/src/lib/services/session-storage.service.ts index 73d7825..b2fad35 100644 --- a/frontend/projects/shared-meet-components/src/lib/services/session-storage.service.ts +++ b/frontend/projects/shared-meet-components/src/lib/services/session-storage.service.ts @@ -8,35 +8,32 @@ import { Injectable } from '@angular/core'; * Provides methods to store, retrieve, and remove data from sessionStorage. */ export class SessionStorageService { - constructor() {} + private readonly ROOM_SECRET_KEY = 'ovMeet-roomSecret'; + private readonly REDIRECT_URL_KEY = 'ovMeet-redirectUrl'; /** - * Stores a secret associated with a participant role for a specific room. + * Stores the room secret. * - * @param roomId The room ID. * @param secret The secret to store. */ - public setRoomSecret(roomId: string, secret: string): void { - this.set(`room_secret_${roomId}`, secret); + public setRoomSecret(secret: string): void { + this.set(this.ROOM_SECRET_KEY, secret); } /** - * Retrieves the room secret for a specific room. + * Retrieves the room secret. * - * @param roomId The room ID. * @returns The stored secret or null if not found. */ - public getRoomSecret(roomId: string): string | null { - return this.get(`room_secret_${roomId}`) ?? null; + public getRoomSecret(): string | null { + return this.get(this.ROOM_SECRET_KEY); } /** - * Removes the room secret for a specific room. - * - * @param roomId The room ID. + * Removes the room secret. */ - public removeRoomSecret(roomId: string): void { - this.remove(`room_secret_${roomId}`); + public removeRoomSecret(): void { + this.remove(this.ROOM_SECRET_KEY); } /** @@ -45,7 +42,7 @@ export class SessionStorageService { * @param redirectUrl The URL to redirect to. */ public setRedirectUrl(redirectUrl: string): void { - this.set('redirect_url', redirectUrl); + this.set(this.REDIRECT_URL_KEY, redirectUrl); } /** @@ -54,7 +51,7 @@ export class SessionStorageService { * @returns The redirect URL or null if not found. */ public getRedirectUrl(): string | null { - return this.get('redirect_url') ?? null; + return this.get(this.REDIRECT_URL_KEY); } /** diff --git a/frontend/projects/shared-meet-components/src/lib/services/token-storage.service.ts b/frontend/projects/shared-meet-components/src/lib/services/token-storage.service.ts new file mode 100644 index 0000000..905791d --- /dev/null +++ b/frontend/projects/shared-meet-components/src/lib/services/token-storage.service.ts @@ -0,0 +1,76 @@ +import { Injectable } from '@angular/core'; + +/** + * Service to manage JWT token storage when using header-based authentication. + * Tokens are stored in localStorage/sessionStorage when authTransportMode is 'header'. + */ +@Injectable({ + providedIn: 'root' +}) +export class TokenStorageService { + private readonly ACCESS_TOKEN_KEY = 'ovMeet-accessToken'; + private readonly REFRESH_TOKEN_KEY = 'ovMeet-refreshToken'; + private readonly PARTICIPANT_TOKEN_KEY = 'ovMeet-participantToken'; + private readonly RECORDING_TOKEN_KEY = 'ovMeet-recordingToken'; + + // ACCESS AND REFRESH TOKEN METHODS + + // Saves the access token to localStorage + setAccessToken(token: string): void { + localStorage.setItem(this.ACCESS_TOKEN_KEY, token); + } + + // Retrieves the access token from localStorage + getAccessToken(): string | null { + return localStorage.getItem(this.ACCESS_TOKEN_KEY); + } + + // Saves the refresh token to localStorage + setRefreshToken(token: string): void { + localStorage.setItem(this.REFRESH_TOKEN_KEY, token); + } + + // Retrieves the refresh token from localStorage + getRefreshToken(): string | null { + return localStorage.getItem(this.REFRESH_TOKEN_KEY); + } + + // Clears access and refresh tokens from localStorage + clearAccessAndRefreshTokens(): void { + localStorage.removeItem(this.ACCESS_TOKEN_KEY); + localStorage.removeItem(this.REFRESH_TOKEN_KEY); + } + + // PARTICIPANT AND RECORDING TOKEN METHODS + // Uses sessionStorage instead of localStorage to ensure tokens are not shared across browser tabs + + // Saves the participant token to sessionStorage + setParticipantToken(token: string): void { + sessionStorage.setItem(this.PARTICIPANT_TOKEN_KEY, token); + } + + // Retrieves the participant token from sessionStorage + getParticipantToken(): string | null { + return sessionStorage.getItem(this.PARTICIPANT_TOKEN_KEY); + } + + // Removes the participant token from sessionStorage + clearParticipantToken(): void { + sessionStorage.removeItem(this.PARTICIPANT_TOKEN_KEY); + } + + // Saves the recording token to sessionStorage + setRecordingToken(token: string): void { + sessionStorage.setItem(this.RECORDING_TOKEN_KEY, token); + } + + // Retrieves the recording token from sessionStorage + getRecordingToken(): string | null { + return sessionStorage.getItem(this.RECORDING_TOKEN_KEY); + } + + // Removes the recording token from sessionStorage + clearRecordingToken(): void { + sessionStorage.removeItem(this.RECORDING_TOKEN_KEY); + } +} diff --git a/typings/src/auth-config.ts b/typings/src/auth-config.ts index 96ea75c..0893ae0 100644 --- a/typings/src/auth-config.ts +++ b/typings/src/auth-config.ts @@ -1,38 +1,47 @@ export interface AuthenticationConfig { - authMethod: ValidAuthMethod; - authModeToAccessRoom: AuthMode; + authMethod: ValidAuthMethod; + authTransportMode: AuthTransportMode; + authModeToAccessRoom: AuthMode; +} + +/** + * Authentication transport modes for JWT tokens. + */ +export const enum AuthTransportMode { + COOKIE = 'cookie', // JWT sent via cookies + HEADER = 'header' // JWT sent via Authorization header } /** * Authentication modes available to enter a room. */ export const enum AuthMode { - NONE = 'none', // No authentication required - MODERATORS_ONLY = 'moderators_only', // Only moderators need authentication - ALL_USERS = 'all_users', // All users need authentication + NONE = 'none', // No authentication required + MODERATORS_ONLY = 'moderators_only', // Only moderators need authentication + ALL_USERS = 'all_users' // All users need authentication } /** * Authentication method base interface. */ export interface AuthMethod { - type: AuthType; + type: AuthType; } /** * Enum for authentication types. */ export const enum AuthType { - SINGLE_USER = 'single_user', - // MULTI_USER = 'multi_user', - // OAUTH_ONLY = 'oauth_only' + SINGLE_USER = 'single_user' + // MULTI_USER = 'multi_user', + // OAUTH_ONLY = 'oauth_only' } /** * Authentication method: Single user with fixed credentials. */ export interface SingleUserAuth extends AuthMethod { - type: AuthType.SINGLE_USER; + type: AuthType.SINGLE_USER; } /** @@ -54,8 +63,7 @@ export interface SingleUserAuth extends AuthMethod { /** * Union type for allowed authentication methods. */ -export type ValidAuthMethod = - SingleUserAuth /* | MultiUserAuth | OAuthOnlyAuth */; +export type ValidAuthMethod = SingleUserAuth /* | MultiUserAuth | OAuthOnlyAuth */; /** * Configuration for OAuth authentication.