Files
upstream-mirror 30810eecd7
Some checks failed
Create issues from TODOs / todos (push) Failing after 3s
Close inactive issues / close-issues (push) Has been cancelled
Deploy GitHub Pages / deploy-preview (push) Has been cancelled
CI / ⚙️ Variables Setup (push) Has been cancelled
CI / ⚙️ Test Guard (push) Has been cancelled
CI / 🚀 Notify external services - draft (push) Has been cancelled
CI / 📦 Build Packages (push) Has been cancelled
CI / 📦 Meteor Build (coverage) (push) Has been cancelled
CI / 📦 Meteor Build (production) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], fips) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Publish Docker Images (ghcr.io) (push) Has been cancelled
CI / 📦 Track Image Sizes (push) Has been cancelled
CI / 🔎 Code Check - Action Lint (push) Has been cancelled
CI / 🔎 Code Check (push) Has been cancelled
CI / 🔨 Test Storybook (push) Has been cancelled
CI / 🔨 Test Unit (push) Has been cancelled
CI / 🔨 Test API (CE) (push) Has been cancelled
CI / 🔨 Test API Livechat (CE) (push) Has been cancelled
CI / 🔨 Test UI (CE) (push) Has been cancelled
CI / 🔨 Test API (EE) (push) Has been cancelled
CI / 🔨 Test API Livechat (EE) (push) Has been cancelled
CI / 🔨 Test API Apps (deno-runtime - EE) (push) Has been cancelled
CI / 🔨 Test UI (EE) (push) Has been cancelled
CI / 🔨 Test API (FIPS) (push) Has been cancelled
CI / 🔨 Test API Livechat (FIPS) (push) Has been cancelled
CI / 🔨 Test UI (FIPS) (push) Has been cancelled
CI / 🔨 Test Federation Matrix (push) Has been cancelled
CI / 📊 Report Coverage (push) Has been cancelled
CI / ✅ Tests Done (push) Has been cancelled
CI / 🚀 Publish build assets (push) Has been cancelled
CI / 🚀 Publish Docker Images (DockerHub) (push) Has been cancelled
CI / 🚀 Notify external services (push) Has been cancelled
CI / Update Version Durability (push) Has been cancelled
Release / Release (push) Has been cancelled
Auto-close duplicate issues / auto-close-duplicates (push) Failing after 1s
Code scanning - action / CodeQL-Build (push) Failing after 2s
snapshot of https://github.com/RocketChat/Rocket.Chat.git (develop, 2026-09-16; blobs > 131072B omitted)
2026-09-16 01:21:02 +05:00

167 lines
4.1 KiB
TypeScript

import { createPublicKey, verify } from 'node:crypto';
import { serverFetch as fetch } from '@rocket.chat/server-fetch';
type AppleJWK = {
kty: string;
kid: string;
use: string;
alg: string;
n: string;
e: string;
};
type AppleJWTPayload = {
iss: string;
sub: string;
aud: string | string[];
exp: number;
iat: number;
email?: string;
email_verified?: string | boolean;
is_private_email?: string | boolean;
};
const DEFAULT_APPLE_AUDIENCES = ['chat.rocket.ios'];
let cachedKeys: AppleJWK[] | null = null;
let lastFetchTime = 0;
const CACHE_TTL_MS = 1000 * 60 * 60 * 24; // 24 hours
async function getApplePublicKeys(forceRefresh = false): Promise<AppleJWK[]> {
const now = Date.now();
if (!forceRefresh && cachedKeys && now - lastFetchTime < CACHE_TTL_MS) {
return cachedKeys;
}
try {
const response = await fetch('https://appleid.apple.com/auth/keys', {
method: 'GET',
// SECURITY: Hardcoded URL, no SSRF protection needed
ignoreSsrfValidation: true,
});
if (!response.ok) {
throw new Error(`Failed to fetch Apple keys: ${response.status} ${response.statusText}`);
}
const data = (await response.json()) as { keys: AppleJWK[] };
cachedKeys = data.keys;
lastFetchTime = now;
return cachedKeys;
} catch (error) {
if (cachedKeys) {
console.warn('Failed to refresh Apple public keys, using stale cache', error);
return cachedKeys;
}
throw new Error('Could not retrieve Apple public keys', { cause: error });
}
}
function decodeBase64Url(str: string): string {
return Buffer.from(str, 'base64url').toString('utf8');
}
async function verifyAppleJWT(
headerB64: string,
payloadB64: string,
signatureB64: string,
clientId: string,
): Promise<AppleJWTPayload | null> {
const header = JSON.parse(decodeBase64Url(headerB64));
const payload = JSON.parse(decodeBase64Url(payloadB64)) as AppleJWTPayload;
const nowInSeconds = Math.floor(Date.now() / 1000);
if (payload.exp < nowInSeconds) {
console.error('Apple JWT has expired');
return null;
}
if (payload.iss !== 'https://appleid.apple.com') {
console.error('Invalid issuer. Expected https://appleid.apple.com');
return null;
}
const audArray = Array.isArray(payload.aud) ? payload.aud : [payload.aud];
const configuredAudiences = clientId
.split(',')
.map((id) => id.trim())
.filter(Boolean);
const allowedAudiences = Array.from(new Set([...DEFAULT_APPLE_AUDIENCES, ...configuredAudiences]));
const isAudienceValid = allowedAudiences.some((allowedAud) => audArray.includes(allowedAud));
if (!isAudienceValid) {
console.error(`Invalid audience. Expected one of: ${allowedAudiences.join(', ')}`);
return null;
}
let applePublicKeys = await getApplePublicKeys();
let keyData = applePublicKeys.find((k) => k.kid === header.kid);
if (!keyData) {
applePublicKeys = await getApplePublicKeys(true); // Force refresh
keyData = applePublicKeys.find((k) => k.kid === header.kid);
if (!keyData) {
console.error('Matching Key ID (kid) not found in Apple JWKS');
return null;
}
}
try {
const publicKey = createPublicKey({
key: {
kty: keyData.kty,
n: keyData.n,
e: keyData.e,
},
format: 'jwk',
});
const isSignatureValid = verify(
'RSA-SHA256',
Buffer.from(`${headerB64}.${payloadB64}`),
publicKey,
Buffer.from(signatureB64, 'base64url'),
);
return isSignatureValid ? payload : null;
} catch (error) {
console.error('Cryptographic signature verification failed:', error);
return null;
}
}
export async function handleIdentityToken(identityToken: string, clientId: string): Promise<Record<string, any>> {
const parts = identityToken.split('.');
if (parts.length !== 3) {
throw new Error('Malformed identityToken: JWT must have 3 parts');
}
const [headerB64, payloadB64, signatureB64] = parts;
const payload = await verifyAppleJWT(headerB64, payloadB64, signatureB64, clientId);
if (!payload) {
throw new Error('identityToken is not a valid Apple JWT or has expired');
}
if (!payload.sub) {
throw new Error('Insufficient data: Missing subject (sub) in auth response token');
}
const serviceData = {
id: payload.sub,
...payload,
};
return serviceData;
}