Files
Final-Year-Project/Backend/utils/device-token.ts

84 lines
2.1 KiB
TypeScript

import { createHmac, timingSafeEqual } from 'crypto';
import { getRequiredEnv } from './env';
type DeviceRole = 'camera' | 'client';
export type DeviceTokenPayload = {
userId: string;
deviceId: string;
role: DeviceRole;
exp: number;
};
const secret = getRequiredEnv('BETTER_AUTH_SECRET');
const base64UrlEncode = (input: string): string => Buffer.from(input, 'utf8').toString('base64url');
const base64UrlDecode = (input: string): string => Buffer.from(input, 'base64url').toString('utf8');
const sign = (data: string): string => createHmac('sha256', secret).update(data).digest('base64url');
export const createDeviceToken = (
payload: Omit<DeviceTokenPayload, 'exp'>,
ttlSeconds = 60 * 60 * 24 * 30,
): string => {
const body: DeviceTokenPayload = {
...payload,
exp: Math.floor(Date.now() / 1000) + ttlSeconds,
};
const encodedPayload = base64UrlEncode(JSON.stringify(body));
const signature = sign(encodedPayload);
return `${encodedPayload}.${signature}`;
};
export const verifyDeviceToken = (token: string): DeviceTokenPayload | null => {
const [encodedPayload, providedSignature] = token.split('.');
if (!encodedPayload || !providedSignature) {
return null;
}
const expectedSignature = sign(encodedPayload);
const providedBuffer = Buffer.from(providedSignature, 'utf8');
const expectedBuffer = Buffer.from(expectedSignature, 'utf8');
if (providedBuffer.length !== expectedBuffer.length) {
return null;
}
if (!timingSafeEqual(providedBuffer, expectedBuffer)) {
return null;
}
let parsedPayload: unknown;
try {
parsedPayload = JSON.parse(base64UrlDecode(encodedPayload));
} catch {
return null;
}
if (!parsedPayload || typeof parsedPayload !== 'object') {
return null;
}
const payload = parsedPayload as Partial<DeviceTokenPayload>;
if (
typeof payload.userId !== 'string' ||
typeof payload.deviceId !== 'string' ||
(payload.role !== 'camera' && payload.role !== 'client') ||
typeof payload.exp !== 'number'
) {
return null;
}
if (payload.exp <= Math.floor(Date.now() / 1000)) {
return null;
}
return payload as DeviceTokenPayload;
};