open-collaboration-server
Version:
Open Collaboration Server implementation, part of the Open Collaboration Tools project
212 lines (184 loc) • 8.56 kB
text/typescript
// ******************************************************************************
// 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: []
};
export abstract class OAuthEndpoint implements AuthEndpoint {
protected loginRedirectRequests = new Map<string, string>();
protected logger: Logger;
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;
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();
}
}
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);
});
}
}
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);
});
}
}