UNPKG

open-collaboration-server

Version:

Open Collaboration Server implementation, part of the Open Collaboration Tools project

153 lines (134 loc) 5.74 kB
// ****************************************************************************** // Copyright 2024 TypeFox GmbH // This program and the accompanying materials are made available under the // terms of the MIT License, which is available in the project root. // ****************************************************************************** import { inject, injectable, postConstruct } from 'inversify'; import { User, isUser } from './types.js'; import { UserManager } from './user-manager.js'; import * as jose from 'jose'; import { nanoid, customAlphabet } from 'nanoid'; import { Disposable, Emitter, Event } from 'open-collaboration-protocol'; import { Logger } from './utils/logging.js'; import { UserInfo } from './auth-endpoints/auth-endpoint.js'; import { Configuration } from './utils/configuration.js'; import { getLocalFilename } from './collaboration-server.js'; export interface DelayedAuth extends Disposable { update(jwt: string): void; onUpdate: Event<string>; onFail: Event<Error>; jwt?: string; } @injectable() export class CredentialsManager { @inject(UserManager) protected readonly userManager: UserManager; @inject(Logger) protected logger: Logger; @inject(Configuration) protected configuration: Configuration; protected deferredAuths = new Map<string, DelayedAuth>(); protected nanoid = this.generateAlphabet(); @postConstruct() initialize() { if (this.configuration.getValue('oct-jwt-private-key') === undefined) { this.logger.warn('OCT_JWT_PRIVATE_KEY env variable is not set. Using a static key for development purposes.'); } } async confirmUser(confirmToken: string, user: UserInfo): Promise<string> { const auth = this.deferredAuths.get(confirmToken); if (!auth) { throw this.logger.createErrorAndLog('Login timed out'); } const registeredUser = await this.userManager.registerUser(user); const userClaim: User = { id: registeredUser.id, name: registeredUser.name, email: registeredUser.email, authProvider: registeredUser.authProvider }; this.logger.info(`Will generate JWT for user [id: ${userClaim.id} | name: ${userClaim.name} | email: ${userClaim.email}]`); const jwt = await this.generateJwt(userClaim); auth.update(jwt); return jwt; } async startAuth(): Promise<string> { const confirmToken = this.secureId(); const updateEmitter = new Emitter<string>(); const failureEmitter = new Emitter<Error>(); const dispose = () => { clearTimeout(timeout); this.deferredAuths.delete(confirmToken); updateEmitter.dispose(); failureEmitter.dispose(); }; const timeout = setTimeout(() => { failureEmitter.fire(new Error('Request timed out')); dispose(); }, 300_000); // 5 minutes of timeout const delayedAuth: DelayedAuth = { update: jwt => { delayedAuth.jwt = jwt; updateEmitter.fire(jwt); }, onUpdate: updateEmitter.event, onFail: failureEmitter.event, dispose }; this.deferredAuths.set(confirmToken, delayedAuth); return confirmToken; } async getAuth(confirmToken: string): Promise<DelayedAuth | undefined> { return this.deferredAuths.get(confirmToken); } async getUser(token: string): Promise<User | undefined> { const user = await this.verifyJwt(token, isUser); if (typeof user.id !== 'string' || typeof user.name !== 'string') { throw this.logger.createErrorAndLog('User token is not valid'); } return user; } async verifyJwt<T extends object>(jwt: string, verify: (obj: unknown) => obj is T): Promise<T> { const key = this.getJwtPrivateKey(); const { payload } = await jose.jwtVerify(jwt, key); if (verify(payload)) { return payload; } else { throw this.logger.createErrorAndLog('JWT payload is not valid'); } } protected getJwtPrivateKey(): Uint8Array { const key = this.configuration.getValue('oct-jwt-private-key') ?? getLocalFilename(import.meta.url); return Buffer.from(key); } async generateJwt(payload: object): Promise<string> { const [key, expiration] = await Promise.all([ this.getJwtPrivateKey(), this.getJwtExpiration() ]); const signJwt = new jose.SignJWT(payload as jose.JWTPayload) .setProtectedHeader({ alg: 'HS256' }) .setIssuedAt(); if (expiration !== undefined) { signJwt.setExpirationTime(expiration); } return signJwt.sign(key); } protected async getJwtExpiration(): Promise<string | number | undefined> { return undefined; } protected generateAlphabet(): typeof nanoid { let alphabet = ''; for (let digit = 48 /* '0' */; digit <= 57 /* '9' */; digit++) { alphabet += String.fromCharCode(digit); } for (let letter = 65 /* 'A' */; letter <= 90 /* 'Z' */; letter++) { alphabet += String.fromCharCode(letter); } for (let letter = 97 /* 'a' */; letter <= 122 /* 'z' */; letter++) { alphabet += String.fromCharCode(letter); } return customAlphabet(alphabet, 24); } secureId(): string { return this.nanoid(24); } }