Implement authentication transport modes for JWT tokens
- Added AuthTransportMode enum to define COOKIE and HEADER modes. - Updated AuthenticationConfig interface to include authTransportMode. - Refactored token handling in participant and recording services to support header-based authentication. - Introduced TokenStorageService for managing JWT tokens in localStorage and sessionStorage. - Modified middleware and controllers to utilize new token extraction methods based on transport mode. - Updated frontend services and components to handle token storage and retrieval according to the selected transport mode. - Enhanced error handling and logging for authentication processes.
This commit is contained in:
parent
5d8343d75d
commit
0cab67eb65
@ -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',
|
||||
|
||||
@ -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,6 +32,19 @@ 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);
|
||||
|
||||
logger.info(`Login succeeded for user '${username}'`);
|
||||
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,
|
||||
@ -44,25 +58,34 @@ export const login = async (req: Request, res: Response) => {
|
||||
INTERNAL_CONFIG.REFRESH_TOKEN_EXPIRATION
|
||||
)
|
||||
);
|
||||
logger.info(`Login succeeded for user '${username}'`);
|
||||
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) => {
|
||||
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);
|
||||
|
||||
logger.info(`Access token refreshed for user '${username}'`);
|
||||
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)
|
||||
);
|
||||
logger.info(`Access token refreshed for user '${username}'`);
|
||||
return res.status(200).json({ message: `Access token for user '${username}' successfully refreshed` });
|
||||
}
|
||||
} catch (error) {
|
||||
handleError(res, error, 'refreshing token');
|
||||
}
|
||||
|
||||
@ -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' });
|
||||
|
||||
@ -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);
|
||||
|
||||
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
|
||||
);
|
||||
|
||||
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}'`);
|
||||
|
||||
@ -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)
|
||||
);
|
||||
}
|
||||
|
||||
return res.status(200).json({ token });
|
||||
} catch (error) {
|
||||
handleError(res, error, `generating recording token for room '${roomId}'`);
|
||||
|
||||
@ -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<void>)[]): 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<User> => {
|
||||
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 {
|
||||
|
||||
@ -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<AuthMode> = z.enum([AuthMode.NONE, AuthMode.MODERATORS_ONLY, AuthMode.ALL_USERS]);
|
||||
|
||||
const AuthTypeSchema: z.ZodType<AuthType> = z.enum([AuthType.SINGLE_USER]);
|
||||
@ -51,6 +54,7 @@ const ValidAuthMethodSchema: z.ZodType<ValidAuthMethod> = SingleUserAuthSchema;
|
||||
|
||||
const AuthenticationConfigSchema: z.ZodType<AuthenticationConfig> = z.object({
|
||||
authMethod: ValidAuthMethodSchema,
|
||||
authTransportMode: AuthTransportModeSchema,
|
||||
authModeToAccessRoom: AuthModeSchema
|
||||
});
|
||||
|
||||
|
||||
@ -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: {
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
export * from './array.utils.js';
|
||||
export * from './cookie.utils.js';
|
||||
export * from './token.utils.js';
|
||||
export * from './url.utils.js';
|
||||
|
||||
121
backend/src/utils/token.utils.ts
Normal file
121
backend/src/utils/token.utils.ts
Normal file
@ -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<AuthTransportMode> => {
|
||||
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<string | undefined> => {
|
||||
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<string | undefined> => {
|
||||
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<string | undefined> => {
|
||||
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<string | undefined> => {
|
||||
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<string | undefined> => {
|
||||
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;
|
||||
}
|
||||
}
|
||||
};
|
||||
@ -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);
|
||||
|
||||
@ -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<unknown>,
|
||||
tokenStorageService: TokenStorageService
|
||||
): HttpRequest<unknown> => {
|
||||
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<unknown>, 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<unknown>, 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<unknown>, ne
|
||||
return from(recordingService.generateRecordingToken(roomId, secret)).pipe(
|
||||
switchMap(() => {
|
||||
console.log('Recording token refreshed');
|
||||
req = addAuthHeadersIfNeeded(req, tokenStorageService);
|
||||
return next(req);
|
||||
}),
|
||||
catchError((error: HttpErrorResponse) => {
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
|
||||
@ -28,6 +28,7 @@ import {
|
||||
RecordingService,
|
||||
RoomService,
|
||||
SessionStorageService,
|
||||
TokenStorageService,
|
||||
WebComponentManagerService
|
||||
} from '@lib/services';
|
||||
import {
|
||||
@ -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
|
||||
|
||||
@ -138,6 +138,4 @@ export class ViewRecordingComponent implements OnInit {
|
||||
this.router.navigate(['/recordings']);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@ -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<any>(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<any> {
|
||||
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<string, string> | undefined;
|
||||
if (refreshToken) {
|
||||
headers = {};
|
||||
headers['x-refresh-token'] = `Bearer ${refreshToken}`;
|
||||
}
|
||||
|
||||
const response = await this.httpService.postRequest<any>(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);
|
||||
|
||||
@ -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<AuthTransportMode> {
|
||||
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);
|
||||
|
||||
@ -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';
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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';
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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<string>(`room_secret_${roomId}`) ?? null;
|
||||
public getRoomSecret(): string | null {
|
||||
return this.get<string>(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<string>('redirect_url') ?? null;
|
||||
return this.get<string>(this.REDIRECT_URL_KEY);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -1,15 +1,24 @@
|
||||
export interface AuthenticationConfig {
|
||||
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
|
||||
ALL_USERS = 'all_users' // All users need authentication
|
||||
}
|
||||
|
||||
/**
|
||||
@ -23,7 +32,7 @@ export interface AuthMethod {
|
||||
* Enum for authentication types.
|
||||
*/
|
||||
export const enum AuthType {
|
||||
SINGLE_USER = 'single_user',
|
||||
SINGLE_USER = 'single_user'
|
||||
// MULTI_USER = 'multi_user',
|
||||
// OAUTH_ONLY = 'oauth_only'
|
||||
}
|
||||
@ -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.
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user