UNPKG

open-collaboration-server

Version:

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

212 lines (184 loc) 8.56 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 { type Express } from 'express'; import { AuthProvider, Emitter, Event, Info } from 'open-collaboration-protocol'; import { AuthEndpoint, AuthSuccessEvent, UserInfo } from './auth-endpoint.js'; import passport from 'passport'; import { Strategy as GithubStrategy } from 'passport-github'; import { Strategy as GoogleStrategy } from 'passport-google-oauth20'; import { Logger } from '../utils/logging.js'; import { Configuration } from '../utils/configuration.js'; import { URL } from 'url'; export const oauthProviders = Symbol('oauthProviders'); export const ThirdParty: Info = { code: Info.Codes.ThirdParty, message: 'Third-party', params: [] }; @injectable() export abstract class OAuthEndpoint implements AuthEndpoint { protected loginRedirectRequests = new Map<string, string>(); @inject(Logger) protected logger: Logger; @inject(Configuration) protected configuration: Configuration; protected abstract id: string; protected abstract path: string protected abstract redirectPath: string protected scope?: string; protected baseURL?: string; private authSuccessEmitter = new Emitter<AuthSuccessEvent>(); onDidAuthenticate: Event<AuthSuccessEvent> = this.authSuccessEmitter.event; abstract getProtocolProvider(): AuthProvider; @postConstruct() initialize() { this.baseURL = this.configuration.getValue('oct-base-url'); } abstract shouldActivate(): boolean; abstract getStrategy(host: string, port: number): passport.Strategy; onStart(app: Express, hostname: string, port: number): void { passport.use(this.id, this.getStrategy(hostname, port)); app.get(this.path, async (req, res) => { const token = req.query.token; if (!token) { this.logger.error('missing token parameter in request'); res.status(400); res.send('Error: Missing token parameter in request'); return; } if(req.query.redirect) { this.loginRedirectRequests.set(token.toString(), req.query.redirect.toString()); } passport.authenticate(this.id, { state: `${token}`, scope: this.scope })(req, res); }); const loginSuccessURL = this.configuration.getValue('oct-login-success-url'); const redirectUriWhitelist = this.configuration.getValue('oct-redirect-url-whitelist')?.split(','); app.get(this.redirectPath, async (req, res) => { const token = (req.query.state as string); if (!token) { this.logger.error('missing token in request state'); res.status(400); res.send('Error: Missing token in request state'); return; } passport.authenticate(this.id, { state: token, scope: this.scope }, async (err: any, userInfo?: UserInfo) => { if (err || !userInfo) { this.logger.error('Error retrieving user info', err); res.status(400); res.send('Error retrieving user info'); return; } try { await Promise.all(this.authSuccessEmitter.fire({ token, userInfo })); } catch (err) { this.logger.error('Error during login', err); res.status(500); res.send('Internal server error occured during Login. Please try again'); return; } const redirectRequest = this.loginRedirectRequests.get(token); if(redirectRequest) { this.loginRedirectRequests.delete(token); if(!redirectUriWhitelist?.includes(redirectRequest)) { this.logger.error(`Redirect URI ${redirectRequest} not in whitelist`); res.status(400); res.send('Error: Redirect URL not in whitelist'); } else { const url = URL.canParse(redirectRequest) ? new URL(redirectRequest) : undefined; if(!url) { res.status(400); res.send('Error: Invalid redirect URL'); return; } url.searchParams.append('token', token); res.redirect(url.toString()); } } else if (loginSuccessURL) { res.redirect(loginSuccessURL); } else { res.status(200); res.send('Login Successful. You can close this page'); } })(req, res); }); } protected createRedirectUrl(host: string, port: number, path: string): string { const baseURL = this.baseURL ?? `http://${host === '0.0.0.0' ? 'localhost' : host}:${port}`; return new URL(path, baseURL).toString(); } } @injectable() export class GitHubOAuthEndpoint extends OAuthEndpoint { protected id = 'github'; protected path = '/api/login/github'; protected redirectPath = '/api/login/github-callback'; shouldActivate(): boolean { return Boolean(this.configuration.getValue('oct-oauth-github-clientid') && this.configuration.getValue('oct-oauth-github-clientsecret')); } override getProtocolProvider(): AuthProvider { return { type: 'web', name: 'github', endpoint: this.path, label: { code: Info.Codes.GitHubLabel, message: 'GitHub', params: [] }, group: ThirdParty }; } override getStrategy(hostname: string, port: number): passport.Strategy { return new GithubStrategy({ clientID: this.configuration.getValue('oct-oauth-github-clientid')!, clientSecret: this.configuration.getValue('oct-oauth-github-clientsecret')!, callbackURL: this.createRedirectUrl(hostname, port, this.redirectPath), }, (accessToken, refreshToken, profile, done) => { const userInfo: UserInfo = { name: profile.displayName, email: profile.emails?.[0]?.value, authProvider: 'Github' }; done(undefined, userInfo); }); } } @injectable() export class GoogleOAuthEndpoint extends OAuthEndpoint { protected id = 'google'; protected path = '/api/login/google'; protected redirectPath = '/api/login/google-callback'; protected override scope = 'email profile'; shouldActivate(): boolean { return Boolean(this.configuration.getValue('oct-oauth-google-clientid') && this.configuration.getValue('oct-oauth-google-clientsecret')); } override getProtocolProvider(): AuthProvider { return { type: 'web', name: 'google', endpoint: this.path, label: { code: Info.Codes.GoogleLabel, message: 'Google', params: [] }, group: ThirdParty }; } override getStrategy(hostname: string, port: number): passport.Strategy { return new GoogleStrategy({ clientID: this.configuration.getValue('oct-oauth-google-clientid')!, clientSecret: this.configuration.getValue('oct-oauth-google-clientsecret')!, callbackURL: this.createRedirectUrl(hostname, port, this.redirectPath), }, (accessToken, refreshToken, profile, done) => { const userInfo: UserInfo = { name: profile.displayName, email: profile.emails?.find(mail => mail.verified)?.value, authProvider: 'Google' }; done(undefined, userInfo); }); } }