import { authentication, AuthenticationGetSessionOptions, AuthenticationProvider, AuthenticationProviderAuthenticationSessionsChangeEvent, AuthenticationSession, Disposable, Event, EventEmitter, SecretStorage, window, } from 'vscode'; class AzureDevOpsPatSession implements AuthenticationSession { // We don't know the user's account name, so we'll just use a constant readonly account = { id: AzureDevOpsAuthenticationProvider.id, label: 'Personal Access Token' }; // This id isn't used for anything in this example, so we set it to a constant readonly id = AzureDevOpsAuthenticationProvider.id; // We don't know what scopes the PAT has, so we have an empty array here. readonly scopes = []; /** * * @param accessToken The personal access token to use for authentication */ constructor(public readonly accessToken: string) { } } export class AzureDevOpsAuthenticationProvider implements AuthenticationProvider, Disposable { static id = 'azuredevopspat'; private static secretKey = 'AzureDevOpsPAT'; // this property is used to determine if the token has been changed in another window of VS Code. // It is used in the checkForUpdates function. private currentToken: Promise | undefined; private initializedDisposable: Disposable | undefined; private _onDidChangeSessions = new EventEmitter(); get onDidChangeSessions(): Event { return this._onDidChangeSessions.event; } constructor(private readonly secretStorage: SecretStorage) { } dispose(): void { this.initializedDisposable?.dispose(); } private ensureInitialized(): void { if (this.initializedDisposable === undefined) { void this.cacheTokenFromStorage(); this.initializedDisposable = Disposable.from( // This onDidChange event happens when the secret storage changes in _any window_ since // secrets are shared across all open windows. this.secretStorage.onDidChange(e => { if (e.key === AzureDevOpsAuthenticationProvider.secretKey) { void this.checkForUpdates(); } }), // This fires when the user initiates a "silent" auth flow via the Accounts menu. authentication.onDidChangeSessions(e => { if (e.provider.id === AzureDevOpsAuthenticationProvider.id) { void this.checkForUpdates(); } }), ); } } // This is a crucial function that handles whether or not the token has changed in // a different window of VS Code and sends the necessary event if it has. private async checkForUpdates(): Promise { const added: AuthenticationSession[] = []; const removed: AuthenticationSession[] = []; const changed: AuthenticationSession[] = []; const previousToken = await this.currentToken; const session = (await this.getSessions(undefined))[0]; if (session?.accessToken && !previousToken) { added.push(session); } else if (!session?.accessToken && previousToken) { removed.push(session); } else if (session?.accessToken !== previousToken) { changed.push(session); } else { return; } void this.cacheTokenFromStorage(); this._onDidChangeSessions.fire({ added: added, removed: removed, changed: changed }); } private cacheTokenFromStorage() { this.currentToken = this.secretStorage.get(AzureDevOpsAuthenticationProvider.secretKey) as Promise; return this.currentToken; } // This function is called first when `vscode.authentication.getSessions` is called. async getSessions(_scopes: string[] | undefined, _options?: AuthenticationGetSessionOptions): Promise { this.ensureInitialized(); const token = await this.cacheTokenFromStorage(); return token ? [new AzureDevOpsPatSession(token)] : []; } // This function is called after `this.getSessions` is called and only when: // - `this.getSessions` returns nothing but `createIfNone` was set to `true` in `vscode.authentication.getSessions` // - `vscode.authentication.getSessions` was called with `forceNewSession: true` // - The end user initiates the "silent" auth flow via the Accounts menu async createSession(_scopes: string[]): Promise { this.ensureInitialized(); // Prompt for the PAT. const token = await window.showInputBox({ ignoreFocusOut: true, placeHolder: 'Personal access token', prompt: 'Enter an Azure DevOps Personal Access Token (PAT).', password: true, }); // Note: this example doesn't do any validation of the token beyond making sure it's not empty. if (!token) { throw new Error('PAT is required'); } // Don't set `currentToken` here, since we want to fire the proper events in the `checkForUpdates` call await this.secretStorage.store(AzureDevOpsAuthenticationProvider.secretKey, token); console.log('Successfully logged in to Azure DevOps'); return new AzureDevOpsPatSession(token); } // This function is called when the end user signs out of the account. async removeSession(_sessionId: string): Promise { const token = await this.currentToken; if (!token) { return; } await this.secretStorage.delete(AzureDevOpsAuthenticationProvider.secretKey); this._onDidChangeSessions.fire({ removed: [new AzureDevOpsPatSession(token)], added: [], changed: [], }); } }