<commit_before>/*---------------------------------------------------------------------------------------------
 *  Copyright (c) Microsoft Corporation. All rights reserved.
 *  Licensed under the MIT License. See License.txt in the project root for license information.
 *--------------------------------------------------------------------------------------------*/

import * as vscode from 'vscode';
import { v4 as uuid } from 'uuid';
import { Keychain } from './common/keychain';
import { GitHubServer, uriHandler } from './githubServer';
import Logger from './common/logger';
import { arrayEquals } from './common/utils';
import { ExperimentationTelemetry } from './experimentationService';
import TelemetryReporter from 'vscode-extension-telemetry';

interface SessionData {
	id: string;
	account?: {
		label?: string;
		displayName?: string;
		id: string;
	}
	scopes: string[];
	accessToken: string;
}

export enum AuthProviderType {
	github = 'github',
	githubEnterprise = 'github-enterprise'
}

export class GitHubAuthenticationProvider implements vscode.AuthenticationProvider, vscode.Disposable {
	private _sessionChangeEmitter = new vscode.EventEmitter<vscode.AuthenticationProviderAuthenticationSessionsChangeEvent>();
	private _githubServer: GitHubServer;
	private _telemetryReporter: ExperimentationTelemetry;

	private _keychain: Keychain;
	private _sessionsPromise: Promise<vscode.AuthenticationSession[]>;
	private _disposable: vscode.Disposable;

	constructor(private context: vscode.ExtensionContext, private type: AuthProviderType) {
		const { name, version, aiKey } = context.extension.packageJSON as { name: string, version: string, aiKey: string };
		this._telemetryReporter = new ExperimentationTelemetry(context, new TelemetryReporter(name, version, aiKey));

		this._keychain = new Keychain(context, `${type}.auth`);
		this._githubServer = new GitHubServer(type, this._telemetryReporter);

		// Contains the current state of the sessions we have available.
		this._sessionsPromise = this.readAndVerifySessions(true);

		const friendlyName = this.type === AuthProviderType.github ? 'GitHub' : 'GitHub Enterprise';
		this._disposable = vscode.Disposable.from(
			this._telemetryReporter,
			this.type === AuthProviderType.github ? vscode.window.registerUriHandler(uriHandler) : { dispose() { } },
			vscode.commands.registerCommand(`${this.type}.provide-token`, () => this.manuallyProvideToken()),
			vscode.authentication.registerAuthenticationProvider(this.type, friendlyName, this, { supportsMultipleAccounts: false }),
			this.context.secrets.onDidChange(() => this.checkForUpdates())
		);
	}

	dispose() {
		this._disposable.dispose();
	}

	get onDidChangeSessions() {
		return this._sessionChangeEmitter.event;
	}

	async getSessions(scopes?: string[]): Promise<vscode.AuthenticationSession[]> {
		Logger.info(`Getting sessions for ${scopes?.join(',') || 'all scopes'}...`);
		const sessions = await this._sessionsPromise;
		const finalSessions = scopes
			? sessions.filter(session => arrayEquals([...session.scopes].sort(), scopes.sort()))
			: sessions;

		Logger.info(`Got ${finalSessions.length} sessions for ${scopes?.join(',') || 'all scopes'}...`);
		return finalSessions;
	}

	private async afterTokenLoad(token: string): Promise<void> {
		if (this.type === AuthProviderType.github) {
			this._githubServer.checkIsEdu(token);
		}
		if (this.type === AuthProviderType.githubEnterprise) {
			this._githubServer.checkEnterpriseVersion(token);
		}
	}

	private async checkForUpdates() {
		const previousSessions = await this._sessionsPromise;
		this._sessionsPromise = this.readAndVerifySessions(false);
		const storedSessions = await this._sessionsPromise;

		const added: vscode.AuthenticationSession[] = [];
		const removed: vscode.AuthenticationSession[] = [];

		storedSessions.forEach(session => {
			const matchesExisting = previousSessions.some(s => s.id === session.id);
			// Another window added a session to the keychain, add it to our state as well
			if (!matchesExisting) {
				Logger.info('Adding session found in keychain');
				added.push(session);
			}
		});

		previousSessions.forEach(session => {
			const matchesExisting = storedSessions.some(s => s.id === session.id);
			// Another window has logged out, remove from our state
			if (!matchesExisting) {
				Logger.info('Removing session no longer found in keychain');
				removed.push(session);
			}
		});

		if (added.length || removed.length) {
			this._sessionChangeEmitter.fire({ added, removed, changed: [] });
		}
	}

	private async readAndVerifySessions(force: boolean): Promise<vscode.AuthenticationSession[]> {
		let sessionData: SessionData[];
		try {
			Logger.info('Reading sessions from keychain...');
			const storedSessions = await this._keychain.getToken() || await this._keychain.tryMigrate();
			if (!storedSessions) {
				return [];
			}
			Logger.info('Got stored sessions!');

			try {
				sessionData = JSON.parse(storedSessions);
			} catch (e) {
				await this._keychain.deleteToken();
				throw e;
			}
		} catch (e) {
			Logger.error(`Error reading token: ${e}`);
			return [];
		}

		const sessionPromises = sessionData.map(async (session: SessionData) => {
			let userInfo: { id: string, accountName: string } | undefined;
			if (force || !session.account) {
				try {
					userInfo = await this._githubServer.getUserInfo(session.accessToken);
					setTimeout(() => this.afterTokenLoad(session.accessToken), 1000);
					Logger.info(`Verified session with the following scopes: ${session.scopes}`);
				} catch (e) {
					// Remove sessions that return unauthorized response
					if (e.message === 'Unauthorized') {
						return undefined;
					}
				}
			}<commit_msg>no longer call /user to verify tokens<commit_after>/*---------------------------------------------------------------------------------------------
 *  Copyright (c) Microsoft Corporation. All rights reserved.
 *  Licensed under the MIT License. See License.txt in the project root for license information.
 *--------------------------------------------------------------------------------------------*/

import * as vscode from 'vscode';
import { v4 as uuid } from 'uuid';
import { Keychain } from './common/keychain';
import { GitHubServer, uriHandler } from './githubServer';
import Logger from './common/logger';
import { arrayEquals } from './common/utils';
import { ExperimentationTelemetry } from './experimentationService';
import TelemetryReporter from 'vscode-extension-telemetry';

interface SessionData {
	id: string;
	account?: {
		label?: string;
		displayName?: string;
		id: string;
	}
	scopes: string[];
	accessToken: string;
}

export enum AuthProviderType {
	github = 'github',
	githubEnterprise = 'github-enterprise'
}

export class GitHubAuthenticationProvider implements vscode.AuthenticationProvider, vscode.Disposable {
	private _sessionChangeEmitter = new vscode.EventEmitter<vscode.AuthenticationProviderAuthenticationSessionsChangeEvent>();
	private _githubServer: GitHubServer;
	private _telemetryReporter: ExperimentationTelemetry;

	private _keychain: Keychain;
	private _sessionsPromise: Promise<vscode.AuthenticationSession[]>;
	private _disposable: vscode.Disposable;

	constructor(private context: vscode.ExtensionContext, private type: AuthProviderType) {
		const { name, version, aiKey } = context.extension.packageJSON as { name: string, version: string, aiKey: string };
		this._telemetryReporter = new ExperimentationTelemetry(context, new TelemetryReporter(name, version, aiKey));

		this._keychain = new Keychain(context, `${type}.auth`);
		this._githubServer = new GitHubServer(type, this._telemetryReporter);

		// Contains the current state of the sessions we have available.
		this._sessionsPromise = this.readSessions();

		const friendlyName = this.type === AuthProviderType.github ? 'GitHub' : 'GitHub Enterprise';
		this._disposable = vscode.Disposable.from(
			this._telemetryReporter,
			this.type === AuthProviderType.github ? vscode.window.registerUriHandler(uriHandler) : { dispose() { } },
			vscode.commands.registerCommand(`${this.type}.provide-token`, () => this.manuallyProvideToken()),
			vscode.authentication.registerAuthenticationProvider(this.type, friendlyName, this, { supportsMultipleAccounts: false }),
			this.context.secrets.onDidChange(() => this.checkForUpdates())
		);
	}

	dispose() {
		this._disposable.dispose();
	}

	get onDidChangeSessions() {
		return this._sessionChangeEmitter.event;
	}

	async getSessions(scopes?: string[]): Promise<vscode.AuthenticationSession[]> {
		Logger.info(`Getting sessions for ${scopes?.join(',') || 'all scopes'}...`);
		const sessions = await this._sessionsPromise;
		const finalSessions = scopes
			? sessions.filter(session => arrayEquals([...session.scopes].sort(), scopes.sort()))
			: sessions;

		Logger.info(`Got ${finalSessions.length} sessions for ${scopes?.join(',') || 'all scopes'}...`);
		return finalSessions;
	}

	private async afterTokenLoad(token: string): Promise<void> {
		if (this.type === AuthProviderType.github) {
			this._githubServer.checkIsEdu(token);
		}
		if (this.type === AuthProviderType.githubEnterprise) {
			this._githubServer.checkEnterpriseVersion(token);
		}
	}

	private async checkForUpdates() {
		const previousSessions = await this._sessionsPromise;
		this._sessionsPromise = this.readSessions();
		const storedSessions = await this._sessionsPromise;

		const added: vscode.AuthenticationSession[] = [];
		const removed: vscode.AuthenticationSession[] = [];

		storedSessions.forEach(session => {
			const matchesExisting = previousSessions.some(s => s.id === session.id);
			// Another window added a session to the keychain, add it to our state as well
			if (!matchesExisting) {
				Logger.info('Adding session found in keychain');
				added.push(session);
			}
		});

		previousSessions.forEach(session => {
			const matchesExisting = storedSessions.some(s => s.id === session.id);
			// Another window has logged out, remove from our state
			if (!matchesExisting) {
				Logger.info('Removing session no longer found in keychain');
				removed.push(session);
			}
		});

		if (added.length || removed.length) {
			this._sessionChangeEmitter.fire({ added, removed, changed: [] });
		}
	}

	private async readSessions(): Promise<vscode.AuthenticationSession[]> {
		let sessionData: SessionData[];
		try {
			Logger.info('Reading sessions from keychain...');
			const storedSessions = await this._keychain.getToken() || await this._keychain.tryMigrate();
			if (!storedSessions) {
				return [];
			}
			Logger.info('Got stored sessions!');

			try {
				sessionData = JSON.parse(storedSessions);
			} catch (e) {
				await this._keychain.deleteToken();
				throw e;
			}
		} catch (e) {
			Logger.error(`Error reading token: ${e}`);
			return [];
		}

		const sessionPromises = sessionData.map(async (session: SessionData) => {
			let userInfo: { id: string, accountName: string } | undefined;
			if (!session.account) {
				try {
					userInfo = await this._githubServer.getUserInfo(session.accessToken);
					setTimeout(() => this.afterTokenLoad(session.accessToken), 1000);
					Logger.info(`Verified session with the following scopes: ${session.scopes}`);
				} catch (e) {
					// Remove sessions that return unauthorized response
					if (e.message === 'Unauthorized') {
						return undefined;
					}
				}
			}

			setTimeout(() => this.afterTokenLoad(session.accessToken), 1000);
{'fraction_non_alphanumeric': 0.15652022010655953, 'fraction_numerical': 0.0012228142195824963, 'mean_word_length': 3.886897140418267, 'pattern_counts': {'":': 0, '<': 13, '<?xml version=': 0, '>': 32, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '7312106', 'n_tokens_mistral': 3619, 'n_tokens_neox': 3229, 'n_words': 777}