cosmos-explorer/src/GitHub/GitHubOAuthService.ts
2020-08-06 14:03:46 -05:00

128 lines
4.4 KiB
TypeScript

import ko from "knockout";
import { HttpStatusCodes } from "../Common/Constants";
import * as Logger from "../Common/Logger";
import { configContext } from "../ConfigContext";
import { AuthorizeAccessComponent } from "../Explorer/Controls/GitHub/AuthorizeAccessComponent";
import { ConsoleDataType } from "../Explorer/Menus/NotificationConsole/NotificationConsoleComponent";
import { JunoClient } from "../Juno/JunoClient";
import { isInvalidParentFrameOrigin } from "../Utils/MessageValidation";
import * as NotificationConsoleUtils from "../Utils/NotificationConsoleUtils";
import { GitHubConnectorMsgType, IGitHubConnectorParams } from "./GitHubConnector";
window.addEventListener("message", (event: MessageEvent) => {
if (isInvalidParentFrameOrigin(event)) {
return;
}
const msg = event.data;
if (msg.type === GitHubConnectorMsgType) {
const params = msg.data as IGitHubConnectorParams;
window.dataExplorer.notebookManager?.gitHubOAuthService.finishOAuth(params);
}
});
export interface IGitHubOAuthToken {
// API properties
access_token?: string;
scope?: string;
token_type?: string;
error?: string;
error_description?: string;
}
export class GitHubOAuthService {
private static readonly OAuthEndpoint = "https://github.com/login/oauth/authorize";
private state: string;
private token: ko.Observable<IGitHubOAuthToken>;
constructor(private junoClient: JunoClient) {
this.token = ko.observable<IGitHubOAuthToken>();
}
public async startOAuth(scope: string): Promise<string> {
// If attempting to change scope from "Public & private repos" to "Public only" we need to delete app authorization.
// Otherwise OAuth app still retains the "public & private repos" permissions.
if (
this.token()?.scope === AuthorizeAccessComponent.Scopes.PublicAndPrivate.key &&
scope === AuthorizeAccessComponent.Scopes.Public.key
) {
const logoutSuccessful = await this.logout();
if (!logoutSuccessful) {
return undefined;
}
}
const params = {
scope,
client_id: configContext.GITHUB_CLIENT_ID,
redirect_uri: new URL("./connectToGitHub.html", window.location.href).href,
state: this.resetState()
};
window.open(`${GitHubOAuthService.OAuthEndpoint}?${new URLSearchParams(params).toString()}`);
return params.state;
}
public async finishOAuth(params: IGitHubConnectorParams) {
try {
this.validateState(params.state);
const response = await this.junoClient.getGitHubToken(params.code);
if (response.status === HttpStatusCodes.OK && !response.data.error) {
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Info, "Successfully connected to GitHub");
this.token(response.data);
} else {
let errorMsg = response.data.error;
if (response.data.error_description) {
errorMsg = `${errorMsg}: ${response.data.error_description}`;
}
throw new Error(errorMsg);
}
} catch (error) {
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, `Failed to connect to GitHub: ${error}`);
this.token({ error });
}
}
public getTokenObservable(): ko.Observable<IGitHubOAuthToken> {
return this.token;
}
public async logout(): Promise<boolean> {
try {
const response = await this.junoClient.deleteAppAuthorization(this.token()?.access_token);
if (response.status !== HttpStatusCodes.NoContent) {
throw new Error(`Received HTTP ${response.status}: ${response.data} when deleting app authorization`);
}
this.resetToken();
return true;
} catch (error) {
const message = `Failed to delete app authorization: ${error}`;
Logger.logError(message, "GitHubOAuthService/logout");
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, message);
return false;
}
}
public isLoggedIn(): boolean {
return !!this.token()?.access_token;
}
private resetState(): string {
this.state = Math.floor(Math.random() * Math.floor(Number.MAX_SAFE_INTEGER)).toString();
return this.state;
}
public resetToken() {
this.token(undefined);
}
private validateState(state: string) {
if (state !== this.state) {
throw new Error("State didn't match. Possibility of cross-site request forgery attack.");
}
}
}