Merge vscode source through release 1.79.2 (#23482)

* log when an editor action doesn't run because of enablement

* notebooks create/dispose editors. this means controllers must be created eagerly (😢) and that notebooks need a custom way of plugging comparision keys for session. works unless creating another session for the same cell of a duplicated editor

* Set offSide to sql lang configuration to true (#183461)

* Fixes #181764 (#183550)

* fix typo

* Always scroll down and focus the input (#183557)

* Fixes #180386 (#183561)

* cli: ensure ordering of rpc server messages (#183558)

* cli: ensure ordering of rpc server messages

Sending lots of messages to a stream would block them around the async
tokio mutex, which is "fair" so doesn't preserve ordering. Instead, use
the write_loop approach I introduced to the server_multiplexer for the
same reason some time ago.

* fix clippy

* update for May endgame

* testing: allow invalidateTestResults to take an array (#183569)

* Document `ShareProvider` API proposal (#183568)

* Document `ShareProvider` API proposal

* Remove mention of VS Code from JSDoc

* Add support for rendering svg and md in welcome message (#183580)

* Remove toggle setting more eagerly (#183584)

* rm message abt macOS

* Change text (#183589)

* Change text

* Accidentally changed the wrong file

* cli: improve output for code tunnel status (#183571)

* testing: allow invalidateTestResults to take an array

* cli: improve output for code tunnel status

Fixes #183570

* [json/css/html] update services (#183595)

* Add experimental setting to enable this dialog

* Fix exporting chat model to JSON before it is initialized (#183597)

* minimum scrolling to reveal the next cell on shift+enter (#183600)

do minimum scrolling to reveal the next cell on Execute cell and select next

* Fixing Jupyter notebook issue 13263 (#183527)

fix for the issue, still need to understand why there is strange focusing

* Tweak proposed API JSDoc (#183590)

* Tweak proposed API JSDoc

* workbench -> workspace

* fix ? operator

* Use active editor and show progress when sharing (#183603)

Use active editor and show progress

* use scroll setting variable correctly

* Schedule welcome widget to show once between typing. (#183606)

* Schedule dialog to show once between typing

* Don't re-render if already displayed once

* Add F10 keybinding for debugger step, even on Web. (#183510)

Fixes #181792.
Previously, for Web the keyboard shortcut was Alt-F10, because it was
believed that F10 could not be bound on browsers. This turned out to be
incorrect, so we make the shortcut consistent (F10) with desktop VSCode
which is also what many other debuggers use.
We keep Alt-F10 on web as a secondary keybinding to keep the experience
some web users may have gotten used to by now.

* Also pass process.env

* Restore missing chat clear commands (#183651)

* chore: update electron@22.5.4 (#183716)

* Show remote indicator in web when remoteAuthority is set (#183728)

* feat: .vuerc as json file (#153017)

Co-authored-by: Martin Aeschlimann <martinae@microsoft.com>

* Delete --compatibility=1.63 code from the server (#183738)

* Copy vscode.dev link to tunnel generates an invalid link when an untitled workspace is open (#183739)

* Recent devcontainer display string corrupted on Get Started page (#183740)

* Improve "next codeblock" navigation (#183744)

* Improve "next codeblock" navigation
Operate on the current focused response, or the last one, and scroll to the selected item

* Normalize command title

* Git - run git status if similarityThreshold changes (#183762)

* fix aria-label issue in kb editor

fixes A11y_GradeB_VSCode_Keyboard shortcut reads words together - Blind: Arrow key navigation to row Find the binding keys and  "when" cell data are read together resulting in a word " CTRL + FeditorFocus  instead of CTRL + F editorFocus" #182490

* Status - fix compact padding (#183768)

* Remove angle brackets from VB brackets (#183782)

Fixes #183359

* Update language config schema with more details about brackets. (#183779)

* fix comment (#183812)

* Support for `Notebook` CodeAction Kind (#183457)

* nb kind support -- wip

* allow notebook codeactions around single cell edit check

* move notebook code action type out of editor

---------

Co-authored-by: rebornix <penn.lv@gmail.com>

* cli: fix connection default being applied (#183827)

* cli: bump to openssl 1.1.1u (#183828)

* Implement "delete" action for chat history (#183609)

* Use desired file name when generating new md pasted file paths (#183861)

Fixes #183851

* Default to filename for markdown new file if empty (#183864)

Fixes #183848

* Fix small typo (#183865)

Fixes #183819

* Noop when moving a symbol into the file it is already in (#183866)

Fixes #183793

* Adjust codeAction validation to account for notebook kind (#183859)

* Make JS/TS `go to configuration` commands work on non-`file:` file systems (#183688)

Make `go to project` commands work on non-`file:` file systems

Fixes #183685

* Can't do regex search after opening notebook (#183884)

Fixes #183858

* Default to current dir for `move to file` select (#183875)

Fixes #183870

`showOpenDialog` seems to ignore `defaultUri` if the file doesn't exist

* Use `<...>` style markdown links when needed (#183876)

Fixes #183849

* Remove check for context keys

* Update xterm package

* Enable updating a chat model without triggering incremental typing (#183894)

* Enable chat "move" commands on empty sessions (#183895)

* Enable chat "move" commands on empty sessions
and also imported sessions

* Fix command name

* Fix some chat keybindings on windows (#183896)

* "Revert File" on inactive editors are ignored (fix #177557) (#183903)

* Empty reason while switching profile (fix #183775) (#183904)

* fix https://github.com/microsoft/vscode-internalbacklog/issues/4278 (#183910)

* fix https://github.com/microsoft/vscode/issues/183770 (#183914)

* code --status displays a lot of errors before actual status output (fix #183787) (#183915)

* joh/icy manatee (#183917)

* Use idle value for widget of interactive editor controller

https://github.com/microsoft/vscode/issues/183820

* also make preview editors idle values

https://github.com/microsoft/vscode/issues/183820

* Fix #183777 (#183929)

* Fix #182309 (#183925)

* Tree checkbox item -> items (#183931)

Fixes #183826

* Fixes #183909 (#183940)

* Fix #183837 (#183943)

fix #183837

* Git - fix #183941 (#183944)

* Update xterm.css

Fixes #181242

* chore: add @ulugbekna and @aiday-mar to my-endgame notebook (#183946)

* Revert "When snippet mode is active, make `Tab` not accept suggestion but advance placeholder"

This reverts commit 50a80cdb61511343996ff1d41d0b676c3d329f48.

* revert not focusing completion list when quick suggest happens during snippet

* change `snippetsPreventQuickSuggestions` default to false

* Fix #181446 (#183956)

* fix https://github.com/microsoft/vscode-internalbacklog/issues/4298 (#183957)

* fix: remove extraneous incorrect context keys (#183959)

These were actually getting added in getTestItemContextOverlay, and the test ID was using the extended ID which extensions do not know about.

Fixes #183612

* Fixes https://github.com/microsoft/monaco-editor/issues/3920 (#183960)

* fix https://github.com/microsoft/vscode-internalbacklog/issues/4324 (#183961)

* fix #183030

* fix #180826 (#183962)

* make message more generic for interactive editor help

* .

* fix #183968

* Keep codeblock toolbar visible when focused

* Fix when clause on "Run in terminal" command

* add important info to help menu

* fix #183970

* Set `isRefactoring` for all TS refactoring edits (#183982)

* consolidate

* Disable move to file in TS versions < 5.2 (#183992)

There are still a few key bugs with refactoring. We will  ship this as a preview for TS 5.2+ instead of for 5.1

* Polish query accepting (#183995)

We shouldn't send the same request to Copilot if the query hasn't changed. So if the query is the same, we short circut.

Fixes https://github.com/microsoft/vscode-internalbacklog/issues/4286

Also, when we open in chat, we should use the last accepted query, not what's in the input box.

Fixes https://github.com/microsoft/vscode-internalbacklog/issues/4280

* Allow widget to have focus (#184000)

So that selecting non-code text works.

Fixes https://github.com/microsoft/vscode-internalbacklog/issues/4294

* Fix microsoft/vscode-internalbacklog#4257. Mitigate zindex for zone widgets. (#184001)

* Change welcome dialog contribution to Eventually

* Misc fixes

* Workspace folder picker entry descriptions are suboptimal for some filesystems (fix #183418) (#184018)

* cli - ignore std error unless verbose (#183787) (#184031)

* joh/inquisitive meerkat (#184034)

* only stash sessions that are none empty

https://github.com/microsoft/vscode-internalbacklog/issues/4281

* only unstash a session once - unless new exchanges are made,

https://github.com/microsoft/vscode-internalbacklog/issues/4281

* account for all exchange types

* Improve declared components (#184039)

* make sure to read setting (#184040)

d'oh, related to https://github.com/microsoft/vscode/issues/173387#issuecomment-1571696644

* [html] update service (#184049)

[html] update service. FIxes #181176

* reset context keys on reset/hide (#184042)

fixes https://github.com/microsoft/vscode-internalbacklog/issues/4330

* use `Lazy`, not `IdleValue` for the IE widget held by the eager controller (#184048)

https://github.com/microsoft/vscode/issues/183820

* fix https://github.com/microsoft/vscode-internalbacklog/issues/4333 (#184067)

* use undo-loop instead of undo-edit when discarding chat session (#184063)

* use undo-loop instead of undo-edit when discarding chat session

fixes https://github.com/microsoft/vscode-internalbacklog/issues/4118

* fix tests, wait for correct state

* Add logging to node download (#184070)

Add logging to node download. For #182951

* re-enable default zone widget revealing when showing (#184072)

fixes https://github.com/microsoft/vscode-internalbacklog/issues/4332, also fixes https://github.com/microsoft/vscode-internalbacklog/issues/3784

* fix #178202

* Allow APIs in stable (#184062)

* Fix microsoft/vscode-internalbacklog#4206. Override List view whitespace css for monaco editor (#184087)

* Fix JSDoc grammatical error (#184090)

* Pick up TS 5.1.3 (#184091)

Fixes #182931

* Misc fixes

* update distro (#184097)

* chore: update electron@22.5.5 (#184116)

* Extension host veto is registered multiple times on restart (fix #183778) (#184127)

Extension host veto is registered multiple times on restart (#183778)

* Do not auto start the local web worker extension host (#184137)

* Allow embedders to intercept trustedTypes.createPolicy calls (#184136)

Allow embedders to intercept trustedTypes.createPolicy calls (#184100)

* fix: reading from console output for --status on windows and linux (#184138)

fix: reading from console output for --status on windows and linux (#184118)

* Misc fixes

* code --status displays a lot of errors before actual status output (fix #183787) (#184200)

fix 183787

* (cherry-pick to 1.79 from main) Handle galleryExtension failure in featuredExtensionService (#184205)

Handle galleryExtension failure in featuredExtensionService (#184198)

Handle galleryExtension failure

* Fix #184183. Multiple output height updates are skipped. (#184188)

* Post merge init fixes

* Misc build issues

* disable toggle inline diff of `alt` down

https://github.com/microsoft/vscode-internalbacklog/issues/4342

* Take into account already activated extensions when computing running locations (#184303)

Take into account already activated extensions when computing running locations (fixes #184180)

* Avoid `extensionService.getExtension` and use `ActivationKind.Immediate` to allow that URI handling works while resolving (#184310)

Avoid `extensionService.getExtension` and use `ActivationKind.Immediate` to allow that URI handling works while resolving (fixes #182217)

* WIP

* rm fish auto injection

* More breaks

* Fix Port Attributes constructor (#184412)

* WIP

* WIP

* Allow extensions to get at the exports of other extensions during resolving (#184487)

Allow extensions to get at the exports of other extensions during resolving (fixes #184472)

* do not auto finish session when inline chat widgets have focus

re https://github.com/microsoft/vscode-internalbacklog/issues/4354

* fix compile errors caused by new base method

* WIP

* WIP

* WIP

* WIP

* Build errors

* unc - fix path traversal bypass

* Bump version

* cherry-pick prod changes from main

* Disable sandbox

* Build break from merge

* bump version

* Merge pull request #184739 from max06/max06/issue184659

Restore ShellIntegration for fish (#184659)

* Git - only add --find-renames if the value is not the default one (#185053)

Git - only add --find-renames if the value is not the default one (#184992)

* Cherry-pick: Revert changes to render featured extensions when available (#184747)

Revert changes to render featured extensions when available.  (#184573)

* Lower timeouts for experimentation and gallery service

* Revert changes to render extensions when available

* Add audio cues

* fix: disable app sandbox when --no-sandbox is present (#184913)

* fix: disable app sandbox when --no-sandbox is present (#184897)

* fix: loading minimist in packaged builds

* Runtime errors

* UNC allow list checks cannot be disabled in extension host (fix #184989) (#185085)

* UNC allow list checks cannot be disabled in extension host (#184989)

* Update src/vs/base/node/unc.js

Co-authored-by: Robo <hop2deep@gmail.com>

---------

Co-authored-by: Robo <hop2deep@gmail.com>

* Add notebook extension

* Fix mangling issues

* Fix mangling issues

* npm install

* npm install

* Issues blocking bundle

* Fix build folder compile errors

* Fix windows bundle build

* Linting fixes

* Fix sqllint issues

* Update yarn.lock files

* Fix unit tests

* Fix a couple breaks from test fixes

* Bump distro

* redo the checkbox style

* Update linux build container dockerfile

* Bump build image tag

* Bump native watch dog package

* Bump node-pty

* Bump distro

* Fix documnetation error

* Update distro

* redo the button styles

* Update datasource TS

* Add missing yarn.lock files

* Windows setup fix

* Turn off extension unit tests while investigating

* color box style

* Remove appx

* Turn off test log upload

* update dropdownlist style

* fix universal app build error (#23488)

* Skip flaky bufferContext vscode test

---------

Co-authored-by: Johannes <johannes.rieken@gmail.com>
Co-authored-by: Henning Dieterichs <hdieterichs@microsoft.com>
Co-authored-by: Julien Richard <jairbubbles@hotmail.com>
Co-authored-by: Charles Gagnon <chgagnon@microsoft.com>
Co-authored-by: Megan Rogge <merogge@microsoft.com>
Co-authored-by: meganrogge <megan.rogge@microsoft.com>
Co-authored-by: Rob Lourens <roblourens@gmail.com>
Co-authored-by: Connor Peet <connor@peet.io>
Co-authored-by: Joyce Er <joyce.er@microsoft.com>
Co-authored-by: Bhavya U <bhavyau@microsoft.com>
Co-authored-by: Raymond Zhao <7199958+rzhao271@users.noreply.github.com>
Co-authored-by: Martin Aeschlimann <martinae@microsoft.com>
Co-authored-by: Aaron Munger <aamunger@microsoft.com>
Co-authored-by: Aiday Marlen Kyzy <amarlenkyzy@microsoft.com>
Co-authored-by: rebornix <penn.lv@gmail.com>
Co-authored-by: Ole <oler@google.com>
Co-authored-by: Jean Pierre <jeanp413@hotmail.com>
Co-authored-by: Robo <hop2deep@gmail.com>
Co-authored-by: Yash Singh <saiansh2525@gmail.com>
Co-authored-by: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com>
Co-authored-by: Ulugbek Abdullaev <ulugbekna@gmail.com>
Co-authored-by: Alex Ross <alros@microsoft.com>
Co-authored-by: Michael Lively <milively@microsoft.com>
Co-authored-by: Matt Bierner <matb@microsoft.com>
Co-authored-by: Andrea Mah <31675041+andreamah@users.noreply.github.com>
Co-authored-by: Benjamin Pasero <benjamin.pasero@microsoft.com>
Co-authored-by: Sandeep Somavarapu <sasomava@microsoft.com>
Co-authored-by: Daniel Imms <2193314+Tyriar@users.noreply.github.com>
Co-authored-by: Tyler James Leonhardt <me@tylerleonhardt.com>
Co-authored-by: Alexandru Dima <alexdima@microsoft.com>
Co-authored-by: Joao Moreno <Joao.Moreno@microsoft.com>
Co-authored-by: Alan Ren <alanren@microsoft.com>
This commit is contained in:
Karl Burtram
2023-06-27 15:26:51 -07:00
committed by GitHub
parent 7975fda6dd
commit 01e66ab3e6
4335 changed files with 252586 additions and 164604 deletions

View File

@@ -3,26 +3,30 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as randomBytes from 'randombytes';
import * as querystring from 'querystring';
import { Buffer } from 'buffer';
import * as vscode from 'vscode';
import * as nls from 'vscode-nls';
import { v4 as uuid } from 'uuid';
import fetch, { Response } from 'node-fetch';
import Logger from './logger';
import { isSupportedEnvironment, toBase64UrlEncoding } from './utils';
import { sha256 } from './env/node/sha256';
import * as querystring from 'querystring';
import * as path from 'path';
import { isSupportedEnvironment } from './utils';
import { generateCodeChallenge, generateCodeVerifier, randomUUID } from './cryptoUtils';
import { BetterTokenStorage, IDidChangeInOtherWindowEvent } from './betterSecretStorage';
import { LoopbackAuthServer } from './authServer';
import path = require('path');
const localize = nls.loadMessageBundle();
import { LoopbackAuthServer } from './node/authServer';
import { base64Decode } from './node/buffer';
import { fetching } from './node/fetch';
import { UriEventHandler } from './UriEventHandler';
import TelemetryReporter from '@vscode/extension-telemetry';
const redirectUrl = 'https://vscode-redirect.azurewebsites.net/';
const loginEndpointUrl = 'https://login.microsoftonline.com/';
const defaultLoginEndpointUrl = 'https://login.microsoftonline.com/';
const DEFAULT_CLIENT_ID = 'aebc6443-996d-45c2-90f0-388ff96faa56';
const DEFAULT_TENANT = 'organizations';
const MSA_TID = '9188040d-6c67-4c5b-b112-36a304b66dad';
const MSA_PASSTHRU_TID = 'f8cdef31-a31e-4b4a-93e4-5f571e91255a';
const enum MicrosoftAccountType {
AAD = 'aad',
MSA = 'msa',
Unknown = 'unknown'
}
interface IToken {
accessToken?: string; // When unable to refresh due to network problems, the access token becomes undefined
@@ -35,20 +39,21 @@ interface IToken {
account: {
label: string;
id: string;
type: MicrosoftAccountType;
};
scope: string;
sessionId: string; // The account id + the scope
}
interface IStoredSession {
export interface IStoredSession {
id: string;
refreshToken: string;
scope: string; // Scopes are alphabetized and joined with a space
account: {
label?: string;
displayName?: string;
label: string;
id: string;
};
endpoint: string | undefined;
}
export interface ITokenResponse {
@@ -67,6 +72,7 @@ export interface IMicrosoftTokens {
}
interface IScopeData {
originalScopes?: string[];
scopes: string[];
scopeStr: string;
scopesToSend: string;
@@ -74,16 +80,8 @@ interface IScopeData {
tenant: string;
}
export const onDidChangeSessions = new vscode.EventEmitter<vscode.AuthenticationProviderAuthenticationSessionsChangeEvent>();
export const REFRESH_NETWORK_FAILURE = 'Network failure';
class UriEventHandler extends vscode.EventEmitter<vscode.Uri> implements vscode.UriHandler {
public handleUri(uri: vscode.Uri) {
this.fire(uri);
}
}
export class AzureActiveDirectoryService {
// For details on why this is set to 2/3... see https://github.com/microsoft/vscode/issues/133201#issuecomment-966668197
private static REFRESH_TIMEOUT_MODIFIER = 1000 * 2 / 3;
@@ -91,29 +89,31 @@ export class AzureActiveDirectoryService {
private _tokens: IToken[] = [];
private _refreshTimeouts: Map<string, NodeJS.Timeout> = new Map<string, NodeJS.Timeout>();
private _refreshingPromise: Promise<any> | undefined;
private _uriHandler: UriEventHandler;
private _sessionChangeEmitter: vscode.EventEmitter<vscode.AuthenticationProviderAuthenticationSessionsChangeEvent> = new vscode.EventEmitter<vscode.AuthenticationProviderAuthenticationSessionsChangeEvent>();
// Used to keep track of current requests when not using the local server approach.
private _pendingNonces = new Map<string, string[]>();
private _codeExchangePromises = new Map<string, Promise<vscode.AuthenticationSession>>();
private _codeVerfifiers = new Map<string, string>();
private readonly _tokenStorage: BetterTokenStorage<IStoredSession>;
constructor(private _context: vscode.ExtensionContext) {
this._tokenStorage = new BetterTokenStorage('microsoft.login.keylist', _context);
this._uriHandler = new UriEventHandler();
_context.subscriptions.push(vscode.window.registerUriHandler(this._uriHandler));
constructor(
private readonly _logger: vscode.LogOutputChannel,
_context: vscode.ExtensionContext,
private readonly _uriHandler: UriEventHandler,
private readonly _tokenStorage: BetterTokenStorage<IStoredSession>,
private readonly _telemetryReporter: TelemetryReporter,
private readonly _loginEndpointUrl: string = defaultLoginEndpointUrl
) {
_context.subscriptions.push(this._tokenStorage.onDidChangeInOtherWindow((e) => this.checkForUpdates(e)));
}
public async initialize(): Promise<void> {
Logger.info('Reading sessions from secret storage...');
const sessions = await this._tokenStorage.getAll();
Logger.info(`Got ${sessions.length} stored sessions`);
this._logger.info('Reading sessions from secret storage...');
const sessions = await this._tokenStorage.getAll(item => this.sessionMatchesEndpoint(item));
this._logger.info(`Got ${sessions.length} stored sessions`);
const refreshes = sessions.map(async session => {
Logger.trace(`Read the following stored session with scopes: ${session.scope}`);
this._logger.trace(`Read the following stored session with scopes: ${session.scope}`);
const scopes = session.scope.split(' ');
const scopeData: IScopeData = {
scopes,
@@ -132,21 +132,21 @@ export class AzureActiveDirectoryService {
accessToken: undefined,
refreshToken: session.refreshToken,
account: {
label: session.account.label ?? session.account.displayName!,
id: session.account.id
...session.account,
type: MicrosoftAccountType.Unknown
},
scope: session.scope,
sessionId: session.id
});
} else {
vscode.window.showErrorMessage(localize('signOut', "You have been signed out because reading stored authentication information failed."));
Logger.error(e);
vscode.window.showErrorMessage(vscode.l10n.t('You have been signed out because reading stored authentication information failed.'));
this._logger.error(e);
await this.removeSessionByIToken({
accessToken: undefined,
refreshToken: session.refreshToken,
account: {
label: session.account.label ?? session.account.displayName!,
id: session.account.id
...session.account,
type: MicrosoftAccountType.Unknown
},
scope: session.scope,
sessionId: session.id
@@ -158,19 +158,40 @@ export class AzureActiveDirectoryService {
const result = await Promise.allSettled(refreshes);
for (const res of result) {
if (res.status === 'rejected') {
Logger.error(`Failed to initialize stored data: ${res.reason}`);
this._logger.error(`Failed to initialize stored data: ${res.reason}`);
this.clearSessions();
break;
}
}
for (const token of this._tokens) {
/* __GDPR__
"login" : {
"owner": "TylerLeonhardt",
"comment": "Used to determine the usage of the Microsoft Auth Provider.",
"scopes": { "classification": "PublicNonPersonalData", "purpose": "FeatureInsight", "comment": "Used to determine what scope combinations are being requested." },
"accountType": { "classification": "PublicNonPersonalData", "purpose": "FeatureInsight", "comment": "Used to determine what account types are being used." }
}
*/
this._telemetryReporter.sendTelemetryEvent('account', {
// Get rid of guids from telemetry.
scopes: JSON.stringify(token.scope.replace(/[0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12}/i, '{guid}').split(' ')),
accountType: token.account.type
});
}
}
//#region session operations
public get onDidChangeSessions(): vscode.Event<vscode.AuthenticationProviderAuthenticationSessionsChangeEvent> {
return this._sessionChangeEmitter.event;
}
async getSessions(scopes?: string[]): Promise<vscode.AuthenticationSession[]> {
if (!scopes) {
Logger.info('Getting sessions for all scopes...');
this._logger.info('Getting sessions for all scopes...');
const sessions = this._tokens.map(token => this.convertToSessionSync(token));
Logger.info(`Got ${sessions.length} sessions for all scopes...`);
this._logger.info(`Got ${sessions.length} sessions for all scopes...`);
return sessions;
}
@@ -184,13 +205,16 @@ export class AzureActiveDirectoryService {
if (!modifiedScopes.includes('profile')) {
modifiedScopes.push('profile');
}
if (!modifiedScopes.includes('offline_access')) {
modifiedScopes.push('offline_access');
}
modifiedScopes = modifiedScopes.sort();
let modifiedScopesStr = modifiedScopes.join(' ');
Logger.info(`Getting sessions for the following scopes: ${modifiedScopesStr}`);
this._logger.info(`Getting sessions for the following scopes: ${modifiedScopesStr}`);
if (this._refreshingPromise) {
Logger.info('Refreshing in progress. Waiting for completion before continuing.');
this._logger.info('Refreshing in progress. Waiting for completion before continuing.');
try {
await this._refreshingPromise;
} catch (e) {
@@ -205,82 +229,92 @@ export class AzureActiveDirectoryService {
// without an idtoken.
if (!matchingTokens.length) {
const fallbackOrderedScopes = scopes.sort().join(' ');
Logger.trace(`No session found with idtoken scopes... Using fallback scope list of: ${fallbackOrderedScopes}`);
this._logger.trace(`No session found with idtoken scopes... Using fallback scope list of: ${fallbackOrderedScopes}`);
matchingTokens = this._tokens.filter(token => token.scope === fallbackOrderedScopes);
if (matchingTokens.length) {
modifiedScopesStr = fallbackOrderedScopes;
}
}
const clientId = this.getClientId(scopes);
const scopeData: IScopeData = {
clientId,
originalScopes: scopes,
scopes: modifiedScopes,
scopeStr: modifiedScopesStr,
// filter our special scopes
scopesToSend: modifiedScopes.filter(s => !s.startsWith('VSCODE_')).join(' '),
tenant: this.getTenantId(scopes),
};
// If we still don't have a matching token try to get a new token from an existing token by using
// the refreshToken. This is documented here:
// https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-auth-code-flow#refresh-the-access-token
// "Refresh tokens are valid for all permissions that your client has already received consent for."
if (!matchingTokens.length) {
const clientId = this.getClientId(modifiedScopes);
// Get a token with the correct client id.
const token = clientId === DEFAULT_CLIENT_ID
? this._tokens.find(t => t.refreshToken && !t.scope.includes('VSCODE_CLIENT_ID'))
: this._tokens.find(t => t.refreshToken && t.scope.includes(`VSCODE_CLIENT_ID:${clientId}`));
if (token) {
const scopeData: IScopeData = {
clientId,
scopes: modifiedScopes,
scopeStr: modifiedScopesStr,
// filter our special scopes
scopesToSend: modifiedScopes.filter(s => !s.startsWith('VSCODE_')).join(' '),
tenant: this.getTenantId(modifiedScopes),
};
try {
const itoken = await this.refreshToken(token.refreshToken, scopeData);
matchingTokens.push(itoken);
} catch (err) {
Logger.error(`Attempted to get a new session for scopes '${scopeData.scopeStr}' using the existing session with scopes '${token.scope}' but it failed due to: ${err.message ?? err}`);
this._logger.error(`Attempted to get a new session for scopes '${scopeData.scopeStr}' using the existing session with scopes '${token.scope}' but it failed due to: ${err.message ?? err}`);
}
}
}
Logger.info(`Got ${matchingTokens.length} sessions for scopes: ${modifiedScopesStr}`);
return Promise.all(matchingTokens.map(token => this.convertToSession(token)));
this._logger.info(`Got ${matchingTokens.length} sessions for scopes: ${modifiedScopesStr}`);
return Promise.all(matchingTokens.map(token => this.convertToSession(token, scopeData)));
}
public createSession(scopes: string[]): Promise<vscode.AuthenticationSession> {
if (!scopes.includes('openid')) {
scopes.push('openid');
public async createSession(scopes: string[]): Promise<vscode.AuthenticationSession> {
let modifiedScopes = [...scopes];
if (!modifiedScopes.includes('openid')) {
modifiedScopes.push('openid');
}
if (!scopes.includes('email')) {
scopes.push('email');
if (!modifiedScopes.includes('email')) {
modifiedScopes.push('email');
}
if (!scopes.includes('profile')) {
scopes.push('profile');
if (!modifiedScopes.includes('profile')) {
modifiedScopes.push('profile');
}
scopes = scopes.sort();
if (!modifiedScopes.includes('offline_access')) {
modifiedScopes.push('offline_access');
}
modifiedScopes = modifiedScopes.sort();
const scopeData: IScopeData = {
scopes,
scopeStr: scopes.join(' '),
originalScopes: scopes,
scopes: modifiedScopes,
scopeStr: modifiedScopes.join(' '),
// filter our special scopes
scopesToSend: scopes.filter(s => !s.startsWith('VSCODE_')).join(' '),
scopesToSend: modifiedScopes.filter(s => !s.startsWith('VSCODE_')).join(' '),
clientId: this.getClientId(scopes),
tenant: this.getTenantId(scopes),
};
Logger.info(`Logging in for the following scopes: ${scopeData.scopeStr}`);
if (!scopeData.scopes.includes('offline_access')) {
Logger.info('Warning: The \'offline_access\' scope was not included, so the generated token will not be able to be refreshed.');
}
this._logger.info(`Logging in for the following scopes: ${scopeData.scopeStr}`);
const runsRemote = vscode.env.remoteName !== undefined;
const runsServerless = vscode.env.remoteName === undefined && vscode.env.uiKind === vscode.UIKind.Web;
if (runsServerless && this._loginEndpointUrl !== defaultLoginEndpointUrl) {
throw new Error('Sign in to non-public clouds is not supported on the web.');
}
if (runsRemote || runsServerless) {
return this.createSessionWithoutLocalServer(scopeData);
}
try {
return this.createSessionWithLocalServer(scopeData);
const session = await this.createSessionWithLocalServer(scopeData);
this._sessionChangeEmitter.fire({ added: [session], removed: [], changed: [] });
return session;
} catch (e) {
Logger.error(`Error creating session for scopes: ${scopeData.scopeStr} Error: ${e}`);
this._logger.error(`Error creating session for scopes: ${scopeData.scopeStr} Error: ${e}`);
// If the error was about starting the server, try directly hitting the login endpoint instead
if (e.message === 'Error listening to server' || e.message === 'Closed' || e.message === 'Timeout waiting for port') {
@@ -292,8 +326,8 @@ export class AzureActiveDirectoryService {
}
private async createSessionWithLocalServer(scopeData: IScopeData) {
const codeVerifier = toBase64UrlEncoding(randomBytes(32).toString('base64'));
const codeChallenge = toBase64UrlEncoding(await sha256(codeVerifier));
const codeVerifier = generateCodeVerifier();
const codeChallenge = await generateCodeChallenge(codeVerifier);
const qs = new URLSearchParams({
response_type: 'code',
response_mode: 'query',
@@ -304,7 +338,7 @@ export class AzureActiveDirectoryService {
code_challenge_method: 'S256',
code_challenge: codeChallenge,
}).toString();
const loginUrl = `${loginEndpointUrl}${scopeData.tenant}/oauth2/v2.0/authorize?${qs}`;
const loginUrl = `${this._loginEndpointUrl}${scopeData.tenant}/oauth2/v2.0/authorize?${qs}`;
const server = new LoopbackAuthServer(path.join(__dirname, '../media'), loginUrl);
await server.start();
@@ -325,16 +359,16 @@ export class AzureActiveDirectoryService {
private async createSessionWithoutLocalServer(scopeData: IScopeData): Promise<vscode.AuthenticationSession> {
let callbackUri = await vscode.env.asExternalUri(vscode.Uri.parse(`${vscode.env.uriScheme}://vscode.microsoft-authentication`));
const nonce = randomBytes(16).toString('base64');
const nonce = generateCodeVerifier();
const callbackQuery = new URLSearchParams(callbackUri.query);
callbackQuery.set('nonce', encodeURIComponent(nonce));
callbackUri = callbackUri.with({
query: callbackQuery.toString()
});
const state = encodeURIComponent(callbackUri.toString(true));
const codeVerifier = toBase64UrlEncoding(randomBytes(32).toString('base64'));
const codeChallenge = toBase64UrlEncoding(await sha256(codeVerifier));
const signInUrl = `${loginEndpointUrl}${scopeData.tenant}/oauth2/v2.0/authorize`;
const codeVerifier = generateCodeVerifier();
const codeChallenge = await generateCodeChallenge(codeVerifier);
const signInUrl = `${this._loginEndpointUrl}${scopeData.tenant}/oauth2/v2.0/authorize`;
const oauthStartQuery = new URLSearchParams({
response_type: 'code',
client_id: encodeURIComponent(scopeData.clientId),
@@ -369,7 +403,7 @@ export class AzureActiveDirectoryService {
existingPromise = this.handleCodeResponse(scopeData);
} else {
inputBox = vscode.window.createInputBox();
existingPromise = Promise.race([this.handleCodeInputBox(inputBox, codeVerifier, scopeData), this.handleCodeResponse(scopeData)]);
existingPromise = this.handleCodeInputBox(inputBox, codeVerifier, scopeData);
}
this._codeExchangePromises.set(scopeData.scopeStr, existingPromise);
}
@@ -384,22 +418,28 @@ export class AzureActiveDirectoryService {
});
}
public removeSessionById(sessionId: string, writeToDisk: boolean = true): Promise<vscode.AuthenticationSession | undefined> {
Logger.info(`Logging out of session '${sessionId}'`);
public async removeSessionById(sessionId: string, writeToDisk: boolean = true): Promise<vscode.AuthenticationSession | undefined> {
this._logger.info(`Logging out of session '${sessionId}'`);
const tokenIndex = this._tokens.findIndex(token => token.sessionId === sessionId);
if (tokenIndex === -1) {
Logger.info(`Session not found '${sessionId}'`);
this._logger.info(`Session not found '${sessionId}'`);
return Promise.resolve(undefined);
}
const token = this._tokens.splice(tokenIndex, 1)[0];
return this.removeSessionByIToken(token, writeToDisk);
const session = await this.removeSessionByIToken(token, writeToDisk);
if (session) {
this._sessionChangeEmitter.fire({ added: [], removed: [session], changed: [] });
}
return session;
}
public async clearSessions() {
Logger.info('Logging out of all sessions');
this._logger.info('Logging out of all sessions');
this._tokens = [];
await this._tokenStorage.deleteAll();
await this._tokenStorage.deleteAll(item => this.sessionMatchesEndpoint(item));
this._refreshTimeouts.forEach(timeout => {
clearTimeout(timeout);
@@ -421,9 +461,9 @@ export class AzureActiveDirectoryService {
}
const session = this.convertToSessionSync(token);
Logger.info(`Sending change event for session that was removed with scopes: ${token.scope}`);
onDidChangeSessions.fire({ added: [], removed: [session], changed: [] });
Logger.info(`Logged out of session '${token.sessionId}' with scopes: ${token.scope}`);
this._logger.info(`Sending change event for session that was removed with scopes: ${token.scope}`);
this._sessionChangeEmitter.fire({ added: [], removed: [session], changed: [] });
this._logger.info(`Logged out of session '${token.sessionId}' with scopes: ${token.scope}`);
return session;
}
@@ -436,11 +476,11 @@ export class AzureActiveDirectoryService {
this._refreshTimeouts.set(sessionId, setTimeout(async () => {
try {
const refreshedToken = await this.refreshToken(refreshToken, scopeData, sessionId);
Logger.info('Triggering change session event...');
onDidChangeSessions.fire({ added: [], removed: [], changed: [this.convertToSessionSync(refreshedToken)] });
this._logger.info('Triggering change session event...');
this._sessionChangeEmitter.fire({ added: [], removed: [], changed: [this.convertToSessionSync(refreshedToken)] });
} catch (e) {
if (e.message !== REFRESH_NETWORK_FAILURE) {
vscode.window.showErrorMessage(localize('signOut', "You have been signed out because reading stored authentication information failed."));
vscode.window.showErrorMessage(vscode.l10n.t('You have been signed out because reading stored authentication information failed.'));
await this.removeSessionById(sessionId);
}
}
@@ -464,10 +504,10 @@ export class AzureActiveDirectoryService {
try {
if (json.id_token) {
claims = JSON.parse(Buffer.from(json.id_token.split('.')[1], 'base64').toString());
claims = JSON.parse(base64Decode(json.id_token.split('.')[1]));
} else {
Logger.info('Attempting to parse access_token instead since no id_token was included in the response.');
claims = JSON.parse(Buffer.from(json.access_token.split('.')[1], 'base64').toString());
this._logger.info('Attempting to parse access_token instead since no id_token was included in the response.');
claims = JSON.parse(base64Decode(json.access_token.split('.')[1]));
}
} catch (e) {
throw e;
@@ -488,10 +528,11 @@ export class AzureActiveDirectoryService {
idToken: json.id_token,
refreshToken: json.refresh_token,
scope: scopeData.scopeStr,
sessionId: existingId || `${id}/${uuid()}`,
sessionId: existingId || `${id}/${randomUUID()}`,
account: {
label,
id
id,
type: claims.tid === MSA_TID || claims.tid === MSA_PASSTHRU_TID ? MicrosoftAccountType.MSA : MicrosoftAccountType.AAD
}
};
}
@@ -510,31 +551,22 @@ export class AzureActiveDirectoryService {
};
}
private async convertToSession(token: IToken): Promise<vscode.AuthenticationSession> {
private async convertToSession(token: IToken, scopeData: IScopeData): Promise<vscode.AuthenticationSession> {
if (token.accessToken && (!token.expiresAt || token.expiresAt > Date.now())) {
token.expiresAt
? Logger.info(`Token available from cache (for scopes ${token.scope}), expires in ${token.expiresAt - Date.now()} milliseconds`)
: Logger.info('Token available from cache (for scopes ${token.scope})');
? this._logger.info(`Token available from cache (for scopes ${token.scope}), expires in ${token.expiresAt - Date.now()} milliseconds`)
: this._logger.info('Token available from cache (for scopes ${token.scope})');
return {
id: token.sessionId,
accessToken: token.accessToken,
idToken: token.idToken,
account: token.account,
scopes: token.scope.split(' ')
scopes: scopeData.originalScopes ?? scopeData.scopes
};
}
try {
Logger.info(`Token expired or unavailable (for scopes ${token.scope}), trying refresh`);
const scopes = token.scope.split(' ');
const scopeData: IScopeData = {
scopes,
scopeStr: token.scope,
// filter our special scopes
scopesToSend: scopes.filter(s => !s.startsWith('VSCODE_')).join(' '),
clientId: this.getClientId(scopes),
tenant: this.getTenantId(scopes),
};
this._logger.info(`Token expired or unavailable (for scopes ${token.scope}), trying refresh`);
const refreshedToken = await this.refreshToken(token.refreshToken, scopeData, token.sessionId);
if (refreshedToken.accessToken) {
return {
@@ -542,7 +574,8 @@ export class AzureActiveDirectoryService {
accessToken: refreshedToken.accessToken,
idToken: refreshedToken.idToken,
account: token.account,
scopes: token.scope.split(' ')
// We always prefer the original scopes requested since that array is used as a key in the AuthService
scopes: scopeData.originalScopes ?? scopeData.scopes
};
} else {
throw new Error();
@@ -567,7 +600,7 @@ export class AzureActiveDirectoryService {
}
private async doRefreshToken(refreshToken: string, scopeData: IScopeData, sessionId?: string): Promise<IToken> {
Logger.info(`Refreshing token for scopes: ${scopeData.scopeStr}`);
this._logger.info(`Refreshing token for scopes: ${scopeData.scopeStr}`);
const postData = querystring.stringify({
refresh_token: refreshToken,
client_id: scopeData.clientId,
@@ -576,7 +609,7 @@ export class AzureActiveDirectoryService {
});
const proxyEndpoints: { [providerId: string]: string } | undefined = await vscode.commands.executeCommand('workbench.getCodeExchangeProxyEndpoints');
const endpointUrl = proxyEndpoints?.microsoft || loginEndpointUrl;
const endpointUrl = proxyEndpoints?.microsoft || this._loginEndpointUrl;
const endpoint = `${endpointUrl}${scopeData.tenant}/oauth2/v2.0/token`;
try {
@@ -585,8 +618,8 @@ export class AzureActiveDirectoryService {
if (token.expiresIn) {
this.setSessionTimeout(token.sessionId, token.refreshToken, scopeData, token.expiresIn * AzureActiveDirectoryService.REFRESH_TIMEOUT_MODIFIER);
}
await this.setToken(token, scopeData);
Logger.info(`Token refresh success for scopes: ${token.scope}`);
this.setToken(token, scopeData);
this._logger.info(`Token refresh success for scopes: ${token.scope}`);
return token;
} catch (e) {
if (e.message === REFRESH_NETWORK_FAILURE) {
@@ -597,7 +630,7 @@ export class AzureActiveDirectoryService {
}
throw e;
}
Logger.error(`Refreshing token failed (for scopes: ${scopeData.scopeStr}): ${e.message}`);
this._logger.error(`Refreshing token failed (for scopes: ${scopeData.scopeStr}): ${e.message}`);
throw e;
}
}
@@ -633,7 +666,6 @@ export class AzureActiveDirectoryService {
return new Promise((resolve: (value: vscode.AuthenticationSession) => void, reject) => {
uriEventListener = this._uriHandler.event(async (uri: vscode.Uri) => {
try {
console.log(uri.query);
const query = querystring.parse(uri.query);
let { code, nonce } = query;
if (Array.isArray(code)) {
@@ -677,9 +709,9 @@ export class AzureActiveDirectoryService {
private async handleCodeInputBox(inputBox: vscode.InputBox, verifier: string, scopeData: IScopeData): Promise<vscode.AuthenticationSession> {
inputBox.ignoreFocusOut = true;
inputBox.title = localize('pasteCodeTitle', 'Microsoft Authentication');
inputBox.prompt = localize('pasteCodePrompt', 'Provide the authorization code to complete the sign in flow.');
inputBox.placeholder = localize('pasteCodePlaceholder', 'Paste authorization code here...');
inputBox.title = vscode.l10n.t('Microsoft Authentication');
inputBox.prompt = vscode.l10n.t('Provide the authorization code to complete the sign in flow.');
inputBox.placeholder = vscode.l10n.t('Paste authorization code here...');
return new Promise((resolve: (value: vscode.AuthenticationSession) => void, reject) => {
inputBox.show();
inputBox.onDidAccept(async () => {
@@ -700,7 +732,7 @@ export class AzureActiveDirectoryService {
}
private async exchangeCodeForSession(code: string, codeVerifier: string, scopeData: IScopeData): Promise<vscode.AuthenticationSession> {
Logger.info(`Exchanging login code for token for scopes: ${scopeData.scopeStr}`);
this._logger.info(`Exchanging login code for token for scopes: ${scopeData.scopeStr}`);
let token: IToken | undefined;
try {
const postData = querystring.stringify({
@@ -712,34 +744,42 @@ export class AzureActiveDirectoryService {
redirect_uri: redirectUrl
});
const proxyEndpoints: { [providerId: string]: string } | undefined = await vscode.commands.executeCommand('workbench.getCodeExchangeProxyEndpoints');
const endpointUrl = proxyEndpoints?.microsoft || loginEndpointUrl;
let endpointUrl: string;
if (this._loginEndpointUrl !== defaultLoginEndpointUrl) {
// If this is for sovereign clouds, don't try using the proxy endpoint, which supports only public cloud
endpointUrl = this._loginEndpointUrl;
} else {
const proxyEndpoints: { [providerId: string]: string } | undefined = await vscode.commands.executeCommand('workbench.getCodeExchangeProxyEndpoints');
endpointUrl = proxyEndpoints?.microsoft || this._loginEndpointUrl;
}
const endpoint = `${endpointUrl}${scopeData.tenant}/oauth2/v2.0/token`;
const json = await this.fetchTokenResponse(endpoint, postData, scopeData);
Logger.info(`Exchanging login code for token (for scopes: ${scopeData.scopeStr}) succeeded!`);
this._logger.info(`Exchanging login code for token (for scopes: ${scopeData.scopeStr}) succeeded!`);
token = this.convertToTokenSync(json, scopeData);
} catch (e) {
Logger.error(`Error exchanging code for token (for scopes ${scopeData.scopeStr}): ${e}`);
this._logger.error(`Error exchanging code for token (for scopes ${scopeData.scopeStr}): ${e}`);
throw e;
}
if (token.expiresIn) {
this.setSessionTimeout(token.sessionId, token.refreshToken, scopeData, token.expiresIn * AzureActiveDirectoryService.REFRESH_TIMEOUT_MODIFIER);
}
await this.setToken(token, scopeData);
Logger.info(`Login successful for scopes: ${scopeData.scopeStr}`);
return await this.convertToSession(token);
this.setToken(token, scopeData);
this._logger.info(`Login successful for scopes: ${scopeData.scopeStr}`);
return await this.convertToSession(token, scopeData);
}
private async fetchTokenResponse(endpoint: string, postData: string, scopeData: IScopeData): Promise<ITokenResponse> {
let attempts = 0;
while (attempts <= 3) {
attempts++;
let result: Response | undefined;
let result;
let errorMessage: string | undefined;
try {
result = await fetch(endpoint, {
result = await fetching(endpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
@@ -753,7 +793,7 @@ export class AzureActiveDirectoryService {
if (!result || result.status > 499) {
if (attempts > 3) {
Logger.error(`Fetching token failed for scopes (${scopeData.scopeStr}): ${result ? await result.text() : errorMessage}`);
this._logger.error(`Fetching token failed for scopes (${scopeData.scopeStr}): ${result ? await result.text() : errorMessage}`);
break;
}
// Exponential backoff
@@ -776,8 +816,8 @@ export class AzureActiveDirectoryService {
//#region storage operations
private async setToken(token: IToken, scopeData: IScopeData): Promise<void> {
Logger.info(`Setting token for scopes: ${scopeData.scopeStr}`);
private setToken(token: IToken, scopeData: IScopeData): void {
this._logger.info(`Setting token for scopes: ${scopeData.scopeStr}`);
const existingTokenIndex = this._tokens.findIndex(t => t.sessionId === token.sessionId);
if (existingTokenIndex > -1) {
@@ -786,23 +826,67 @@ export class AzureActiveDirectoryService {
this._tokens.push(token);
}
// Don't await because setting the token is only useful for any new windows that open.
this.storeToken(token, scopeData);
}
private async storeToken(token: IToken, scopeData: IScopeData): Promise<void> {
if (!vscode.window.state.focused) {
const shouldStore = await new Promise((resolve, _) => {
// To handle the case where the window is not focused for a long time. We want to store the token
// at some point so that the next time they _do_ interact with VS Code, they don't have to sign in again.
const timer = setTimeout(
() => resolve(true),
// 5 hours + random extra 0-30 seconds so that each window doesn't try to store at the same time
(18000000) + Math.floor(Math.random() * 30000)
);
const dispose = vscode.Disposable.from(
vscode.window.onDidChangeWindowState(e => {
if (e.focused) {
resolve(true);
dispose.dispose();
clearTimeout(timer);
}
}),
this._tokenStorage.onDidChangeInOtherWindow(e => {
if (e.updated.includes(token.sessionId)) {
resolve(false);
dispose.dispose();
clearTimeout(timer);
}
})
);
});
if (!shouldStore) {
this._logger.info(`Not storing token for scopes ${scopeData.scopeStr} because it was added in another window`);
return;
}
}
await this._tokenStorage.store(token.sessionId, {
id: token.sessionId,
refreshToken: token.refreshToken,
scope: token.scope,
account: token.account
account: token.account,
endpoint: this._loginEndpointUrl,
});
this._logger.info(`Stored token for scopes: ${scopeData.scopeStr}`);
}
private async checkForUpdates(e: IDidChangeInOtherWindowEvent<IStoredSession>): Promise<void> {
const added: vscode.AuthenticationSession[] = [];
const removed: vscode.AuthenticationSession[] = [];
for (const key of e.added) {
const session = await this._tokenStorage.get(key);
if (!session) {
Logger.error('session not found that was apparently just added');
this._logger.error('session not found that was apparently just added');
return;
}
if (!this.sessionMatchesEndpoint(session)) {
// If the session wasn't made for this login endpoint, ignore this update
continue;
}
const matchesExisting = this._tokens.some(token => token.scope === session.scope && token.sessionId === session.id);
if (!matchesExisting && session.refreshToken) {
try {
@@ -815,15 +899,15 @@ export class AzureActiveDirectoryService {
clientId: this.getClientId(scopes),
tenant: this.getTenantId(scopes),
};
Logger.info(`Session added in another window with scopes: ${session.scope}`);
this._logger.info(`Session added in another window with scopes: ${session.scope}`);
const token = await this.refreshToken(session.refreshToken, scopeData, session.id);
Logger.info(`Sending change event for session that was added with scopes: ${scopeData.scopeStr}`);
onDidChangeSessions.fire({ added: [this.convertToSessionSync(token)], removed: [], changed: [] });
this._logger.info(`Sending change event for session that was added with scopes: ${scopeData.scopeStr}`);
this._sessionChangeEmitter.fire({ added: [this.convertToSessionSync(token)], removed: [], changed: [] });
return;
} catch (e) {
// Network failures will automatically retry on next poll.
if (e.message !== REFRESH_NETWORK_FAILURE) {
vscode.window.showErrorMessage(localize('signOut', "You have been signed out because reading stored authentication information failed."));
vscode.window.showErrorMessage(vscode.l10n.t('You have been signed out because reading stored authentication information failed.'));
await this.removeSessionById(session.id);
}
return;
@@ -832,33 +916,26 @@ export class AzureActiveDirectoryService {
}
for (const { value } of e.removed) {
Logger.info(`Session removed in another window with scopes: ${value.scope}`);
const session = await this.removeSessionById(value.id, false);
if (session) {
removed.push(session);
if (!this.sessionMatchesEndpoint(value)) {
// If the session wasn't made for this login endpoint, ignore this update
continue;
}
this._logger.info(`Session removed in another window with scopes: ${value.scope}`);
await this.removeSessionById(value.id, false);
}
// NOTE: We don't need to handle changed sessions because all that really would give us is a new refresh token
// because access tokens are not stored in Secret Storage due to their short lifespan. This new refresh token
// is not useful in this window because we really only care about the lifetime of the _access_ token which we
// are already managing (see usages of `setSessionTimeout`).
}
//#endregion
private sessionMatchesEndpoint(session: IStoredSession): boolean {
// For older sessions with no endpoint set, it can be assumed to be the default endpoint
session.endpoint ||= defaultLoginEndpointUrl;
//#region static methods
private static getCallbackEnvironment(callbackUri: vscode.Uri): string {
if (callbackUri.scheme !== 'https' && callbackUri.scheme !== 'http') {
return callbackUri.scheme;
}
switch (callbackUri.authority) {
case 'online.visualstudio.com':
return 'vso';
case 'online-ppe.core.vsengsaas.visualstudio.com':
return 'vsoppe';
case 'online.dev.core.vsengsaas.visualstudio.com':
return 'vsodev';
default:
return callbackUri.authority;
}
return session.endpoint === this._loginEndpointUrl;
}
//#endregion

View File

@@ -0,0 +1,12 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
export class UriEventHandler extends vscode.EventEmitter<vscode.Uri> implements vscode.UriHandler {
public handleUri(uri: vscode.Uri) {
this.fire(uri);
}
}

View File

@@ -81,11 +81,13 @@ export class BetterTokenStorage<T> {
return tokens.get(key);
}
async getAll(): Promise<T[]> {
async getAll(predicate?: (item: T) => boolean): Promise<T[]> {
const tokens = await this.getTokens();
const values = new Array<T>();
for (const [_, value] of tokens) {
values.push(value);
if (!predicate || predicate(value)) {
values.push(value);
}
}
return values;
}
@@ -141,11 +143,13 @@ export class BetterTokenStorage<T> {
this._operationInProgress = false;
}
async deleteAll(): Promise<void> {
async deleteAll(predicate?: (item: T) => boolean): Promise<void> {
const tokens = await this.getTokens();
const promises = [];
for (const [key] of tokens) {
promises.push(this.delete(key));
for (const [key, value] of tokens) {
if (!predicate || predicate(value)) {
promises.push(this.delete(key));
}
}
await Promise.all(promises);
}

View File

@@ -0,0 +1,17 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
export function base64Encode(text: string): string {
return btoa(text);
}
export function base64Decode(text: string): string {
// modification of https://stackoverflow.com/a/38552302
const replacedCharacters = text.replace(/-/g, '+').replace(/_/g, '/');
const decodedText = decodeURIComponent(atob(replacedCharacters).split('').map(function (c) {
return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2);
}).join(''));
return decodedText;
}

View File

@@ -3,8 +3,4 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
export async function sha256(s: string | Uint8Array): Promise<string> {
return (require('crypto')).createHash('sha256').update(s).digest('base64');
}
export const crypto = globalThis.crypto;

View File

@@ -0,0 +1,45 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { base64Encode } from './node/buffer';
import { crypto } from './node/crypto';
export function randomUUID() {
return crypto.randomUUID();
}
function dec2hex(dec: number): string {
return ('0' + dec.toString(16)).slice(-2);
}
export function generateCodeVerifier(): string {
const array = new Uint32Array(56 / 2);
crypto.getRandomValues(array);
return Array.from(array, dec2hex).join('');
}
function sha256(plain: string | undefined) {
const encoder = new TextEncoder();
const data = encoder.encode(plain);
return crypto.subtle.digest('SHA-256', data);
}
function base64urlencode(a: ArrayBuffer) {
let str = '';
const bytes = new Uint8Array(a);
const len = bytes.byteLength;
for (let i = 0; i < len; i++) {
str += String.fromCharCode(bytes[i]);
}
return base64Encode(str)
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=+$/, '');
}
export async function generateCodeChallenge(v: string) {
const hashed = await sha256(v);
const base64encoded = base64urlencode(hashed);
return base64encoded;
}

View File

@@ -4,18 +4,115 @@
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
import { AzureActiveDirectoryService, onDidChangeSessions } from './AADHelper';
import { AzureActiveDirectoryService, IStoredSession } from './AADHelper';
import { BetterTokenStorage } from './betterSecretStorage';
import { UriEventHandler } from './UriEventHandler';
import TelemetryReporter from '@vscode/extension-telemetry';
export async function activate(context: vscode.ExtensionContext) {
const { name, version, aiKey } = context.extension.packageJSON as { name: string; version: string; aiKey: string };
const telemetryReporter = new TelemetryReporter(name, version, aiKey);
async function initMicrosoftSovereignCloudAuthProvider(context: vscode.ExtensionContext, telemetryReporter: TelemetryReporter, uriHandler: UriEventHandler, tokenStorage: BetterTokenStorage<IStoredSession>): Promise<vscode.Disposable | undefined> {
let settingValue = vscode.workspace.getConfiguration('microsoft-sovereign-cloud').get<string | undefined>('endpoint');
let authProviderName: string | undefined;
if (!settingValue) {
return undefined;
} else if (settingValue === 'Azure China') {
authProviderName = settingValue;
settingValue = 'https://login.chinacloudapi.cn/';
} else if (settingValue === 'Azure US Government') {
authProviderName = settingValue;
settingValue = 'https://login.microsoftonline.us/';
}
const loginService = new AzureActiveDirectoryService(context);
// validate user value
let uri: vscode.Uri;
try {
uri = vscode.Uri.parse(settingValue, true);
} catch (e) {
vscode.window.showErrorMessage(vscode.l10n.t('Microsoft Sovereign Cloud login URI is not a valid URI: {0}', e.message ?? e));
return;
}
// Add trailing slash if needed
if (!settingValue.endsWith('/')) {
settingValue += '/';
}
const aadService = new AzureActiveDirectoryService(
vscode.window.createOutputChannel(vscode.l10n.t('Microsoft Sovereign Cloud Authentication'), { log: true }),
context,
uriHandler,
tokenStorage,
telemetryReporter,
settingValue);
await aadService.initialize();
authProviderName ||= uri.authority;
const disposable = vscode.authentication.registerAuthenticationProvider('microsoft-sovereign-cloud', authProviderName, {
onDidChangeSessions: aadService.onDidChangeSessions,
getSessions: (scopes: string[]) => aadService.getSessions(scopes),
createSession: async (scopes: string[]) => {
try {
/* __GDPR__
"login" : {
"owner": "TylerLeonhardt",
"comment": "Used to determine the usage of the Microsoft Sovereign Cloud Auth Provider.",
"scopes": { "classification": "PublicNonPersonalData", "purpose": "FeatureInsight", "comment": "Used to determine what scope combinations are being requested." }
}
*/
telemetryReporter.sendTelemetryEvent('loginMicrosoftSovereignCloud', {
// Get rid of guids from telemetry.
scopes: JSON.stringify(scopes.map(s => s.replace(/[0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12}/i, '{guid}'))),
});
return await aadService.createSession(scopes.sort());
} catch (e) {
/* __GDPR__
"loginFailed" : { "owner": "TylerLeonhardt", "comment": "Used to determine how often users run into issues with the login flow." }
*/
telemetryReporter.sendTelemetryEvent('loginMicrosoftSovereignCloudFailed');
throw e;
}
},
removeSession: async (id: string) => {
try {
/* __GDPR__
"logout" : { "owner": "TylerLeonhardt", "comment": "Used to determine how often users log out." }
*/
telemetryReporter.sendTelemetryEvent('logoutMicrosoftSovereignCloud');
await aadService.removeSessionById(id);
} catch (e) {
/* __GDPR__
"logoutFailed" : { "owner": "TylerLeonhardt", "comment": "Used to determine how often fail to log out." }
*/
telemetryReporter.sendTelemetryEvent('logoutMicrosoftSovereignCloudFailed');
}
}
}, { supportsMultipleAccounts: true });
context.subscriptions.push(disposable);
return disposable;
}
export async function activate(context: vscode.ExtensionContext) {
const aiKey: string = context.extension.packageJSON.aiKey;
const telemetryReporter = new TelemetryReporter(aiKey);
const uriHandler = new UriEventHandler();
context.subscriptions.push(uriHandler);
context.subscriptions.push(vscode.window.registerUriHandler(uriHandler));
const betterSecretStorage = new BetterTokenStorage<IStoredSession>('microsoft.login.keylist', context);
const loginService = new AzureActiveDirectoryService(
vscode.window.createOutputChannel(vscode.l10n.t('Microsoft Authentication'), { log: true }),
context,
uriHandler,
betterSecretStorage,
telemetryReporter);
await loginService.initialize();
context.subscriptions.push(vscode.authentication.registerAuthenticationProvider('microsoft', 'Microsoft', {
onDidChangeSessions: onDidChangeSessions.event,
onDidChangeSessions: loginService.onDidChangeSessions,
getSessions: (scopes: string[]) => loginService.getSessions(scopes),
createSession: async (scopes: string[]) => {
try {
@@ -31,9 +128,7 @@ export async function activate(context: vscode.ExtensionContext) {
scopes: JSON.stringify(scopes.map(s => s.replace(/[0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12}/i, '{guid}'))),
});
const session = await loginService.createSession(scopes.sort());
onDidChangeSessions.fire({ added: [session], removed: [], changed: [] });
return session;
return await loginService.createSession(scopes.sort());
} catch (e) {
/* __GDPR__
"loginFailed" : { "owner": "TylerLeonhardt", "comment": "Used to determine how often users run into issues with the login flow." }
@@ -50,10 +145,7 @@ export async function activate(context: vscode.ExtensionContext) {
*/
telemetryReporter.sendTelemetryEvent('logout');
const session = await loginService.removeSessionById(id);
if (session) {
onDidChangeSessions.fire({ added: [], removed: [session], changed: [] });
}
await loginService.removeSessionById(id);
} catch (e) {
/* __GDPR__
"logoutFailed" : { "owner": "TylerLeonhardt", "comment": "Used to determine how often fail to log out." }
@@ -63,6 +155,15 @@ export async function activate(context: vscode.ExtensionContext) {
}
}, { supportsMultipleAccounts: true }));
let microsoftSovereignCloudAuthProviderDisposable = await initMicrosoftSovereignCloudAuthProvider(context, telemetryReporter, uriHandler, betterSecretStorage);
context.subscriptions.push(vscode.workspace.onDidChangeConfiguration(async e => {
if (e.affectsConfiguration('microsoft-sovereign-cloud.endpoint')) {
microsoftSovereignCloudAuthProviderDisposable?.dispose();
microsoftSovereignCloudAuthProviderDisposable = await initMicrosoftSovereignCloudAuthProvider(context, telemetryReporter, uriHandler, betterSecretStorage);
}
}));
return;
}

View File

@@ -5,55 +5,5 @@
import * as vscode from 'vscode';
type LogLevel = 'Trace' | 'Info' | 'Error';
class Log {
private output: vscode.OutputChannel;
constructor() {
this.output = vscode.window.createOutputChannel('Microsoft Authentication');
}
private data2String(data: any): string {
if (data instanceof Error) {
return data.stack || data.message;
}
if (data.success === false && data.message) {
return data.message;
}
return data.toString();
}
public trace(message: string, data?: any): void {
this.logLevel('Trace', message, data);
}
public info(message: string, data?: any): void {
this.logLevel('Info', message, data);
}
public error(message: string, data?: any): void {
this.logLevel('Error', message, data);
}
public logLevel(level: LogLevel, message: string, data?: any): void {
this.output.appendLine(`[${level} - ${this.now()}] ${message}`);
if (data) {
this.output.appendLine(this.data2String(data));
}
}
private now(): string {
const now = new Date();
return padLeft(now.getUTCHours() + '', 2, '0')
+ ':' + padLeft(now.getMinutes() + '', 2, '0')
+ ':' + padLeft(now.getUTCSeconds() + '', 2, '0') + '.' + now.getMilliseconds();
}
}
function padLeft(s: string, n: number, pad = ' ') {
return pad.repeat(Math.max(0, n - s.length)) + s;
}
const Logger = new Log();
const Logger = vscode.window.createOutputChannel(vscode.l10n.t('Microsoft Authentication'), { log: true });
export default Logger;

View File

@@ -0,0 +1,12 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
export function base64Encode(text: string): string {
return Buffer.from(text, 'binary').toString('base64');
}
export function base64Decode(text: string): string {
return Buffer.from(text, 'base64').toString('utf8');
}

View File

@@ -2,10 +2,6 @@
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as nodeCrypto from 'crypto';
'use strict';
export async function sha256(s: string | Uint8Array): Promise<string> {
const createHash = require('sha.js');
return createHash('sha256').update(s).digest('base64');
}
export const crypto: Crypto = nodeCrypto.webcrypto as any;

View File

@@ -0,0 +1,7 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import fetch from 'node-fetch';
export const fetching = fetch;

View File

@@ -4,10 +4,6 @@
*--------------------------------------------------------------------------------------------*/
import { env, UIKind, Uri } from 'vscode';
export function toBase64UrlEncoding(base64string: string) {
return base64string.replace(/=/g, '').replace(/\+/g, '-').replace(/\//g, '_'); // Need to use base64url encoding
}
const LOCALHOST_ADDRESSES = ['localhost', '127.0.0.1', '0:0:0:0:0:0:0:1', '::1'];
function isLocalhost(uri: Uri): boolean {
if (!/^https?$/i.test(uri.scheme)) {