Skip to content

Commit

Permalink
Merge pull request #106560 from microsoft/sandy081/web-playground/login
Browse files Browse the repository at this point in the history
Enable authentication in web playground
  • Loading branch information
sandy081 committed Sep 16, 2020
2 parents d25064b + 17db0e5 commit 9e50567
Show file tree
Hide file tree
Showing 6 changed files with 102 additions and 63 deletions.
29 changes: 7 additions & 22 deletions resources/web/code-web.js
Original file line number Diff line number Diff line change
Expand Up @@ -369,32 +369,17 @@ async function handleRoot(req, res) {
webConfigJSON._wrapWebWorkerExtHostInIframe = true;
}

const credentials = [];
if (args['github-auth']) {
const sessionId = uuid.v4();
credentials.push({
service: 'code-oss.login',
account: 'account',
password: JSON.stringify({
id: sessionId,
providerId: 'github',
accessToken: args['github-auth']
})
}, {
service: 'code-oss-github.login',
account: 'account',
password: JSON.stringify([{
id: sessionId,
scopes: ['user:email'],
accessToken: args['github-auth']
}])
});
}
const authSessionInfo = args['github-auth'] ? {
id: uuid.v4(),
providerId: 'github',
accessToken: args['github-auth'],
scopes: [['user:email'], ['repo']]
} : undefined;

const data = (await readFile(WEB_MAIN)).toString()
.replace('{{WORKBENCH_WEB_CONFIGURATION}}', () => escapeAttribute(JSON.stringify(webConfigJSON))) // use a replace function to avoid that regexp replace patterns ($&, $0, ...) are applied
.replace('{{WORKBENCH_BUILTIN_EXTENSIONS}}', () => escapeAttribute(JSON.stringify(dedupedBuiltInExtensions)))
.replace('{{WORKBENCH_CREDENTIALS}}', () => escapeAttribute(JSON.stringify(credentials)))
.replace('{{WORKBENCH_AUTH_SESSION}}', () => authSessionInfo ? escapeAttribute(JSON.stringify(authSessionInfo)) : '')
.replace('{{WEBVIEW_ENDPOINT}}', '');


Expand Down
6 changes: 3 additions & 3 deletions src/vs/code/browser/workbench/workbench-dev.html
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,12 @@
<!-- Workbench Configuration -->
<meta id="vscode-workbench-web-configuration" data-settings="{{WORKBENCH_WEB_CONFIGURATION}}">

<!-- Workbench Auth Session -->
<meta id="vscode-workbench-auth-session" data-settings="{{WORKBENCH_AUTH_SESSION}}">

<!-- Builtin Extensions (running out of sources) -->
<meta id="vscode-workbench-builtin-extensions" data-settings="{{WORKBENCH_BUILTIN_EXTENSIONS}}">

<!-- Workbench Credentials (running out of sources) -->
<meta id="vscode-workbench-credentials" data-settings="{{WORKBENCH_CREDENTIALS}}">

<!-- Workbench Icon/Manifest/CSS -->
<link rel="icon" href="/favicon.ico" type="image/x-icon" />
<link rel="manifest" href="/manifest.json">
Expand Down
3 changes: 3 additions & 0 deletions src/vs/code/browser/workbench/workbench.html
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@
<!-- Workbench Configuration -->
<meta id="vscode-workbench-web-configuration" data-settings="{{WORKBENCH_WEB_CONFIGURATION}}">

<!-- Workbench Auth Session -->
<meta id="vscode-workbench-auth-session" data-settings="{{WORKBENCH_AUTH_SESSION}}">

<!-- Workbench Icon/Manifest/CSS -->
<link rel="icon" href="/favicon.ico" type="image/x-icon" />
<link rel="manifest" href="/manifest.json">
Expand Down
120 changes: 84 additions & 36 deletions src/vs/code/browser/workbench/workbench.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,25 @@ import { isEqual } from 'vs/base/common/resources';
import { isStandalone } from 'vs/base/browser/browser';
import { localize } from 'vs/nls';
import { Schemas } from 'vs/base/common/network';
import product from 'vs/platform/product/common/product';

function doCreateUri(path: string, queryValues: Map<string, string>): URI {
let query: string | undefined = undefined;

if (queryValues) {
let index = 0;
queryValues.forEach((value, key) => {
if (!query) {
query = '';
}

const prefix = (index++ === 0) ? '' : '&';
query += `${prefix}${key}=${encodeURIComponent(value)}`;
});
}

return URI.parse(window.location.href).with({ path, query });
}

interface ICredential {
service: string;
Expand All @@ -27,6 +46,32 @@ class LocalStorageCredentialsProvider implements ICredentialsProvider {

static readonly CREDENTIALS_OPENED_KEY = 'credentials.provider';

private readonly authService: string | undefined;

constructor() {
let authSessionInfo: { readonly id: string, readonly accessToken: string, readonly providerId: string, readonly canSignOut?: boolean, readonly scopes: string[][] } | undefined;
const authSessionElement = document.getElementById('vscode-workbench-auth-session');
const authSessionElementAttribute = authSessionElement ? authSessionElement.getAttribute('data-settings') : undefined;
if (authSessionElementAttribute) {
try {
authSessionInfo = JSON.parse(authSessionElementAttribute);
} catch (error) { /* Invalid session is passed. Ignore. */ }
}

if (authSessionInfo) {
// Settings Sync Entry
this.setPassword(`${product.urlProtocol}.login`, 'account', JSON.stringify(authSessionInfo));

// Auth extension Entry
this.authService = `${product.urlProtocol}-${authSessionInfo.providerId}.login`;
this.setPassword(this.authService, 'account', JSON.stringify(authSessionInfo.scopes.map(scopes => ({
id: authSessionInfo!.id,
scopes,
accessToken: authSessionInfo!.accessToken
}))));
}
}

private _credentials: ICredential[] | undefined;
private get credentials(): ICredential[] {
if (!this._credentials) {
Expand Down Expand Up @@ -68,14 +113,39 @@ class LocalStorageCredentialsProvider implements ICredentialsProvider {
}

async setPassword(service: string, account: string, password: string): Promise<void> {
this.deletePassword(service, account);
this.doDeletePassword(service, account);

this.credentials.push({ service, account, password });

this.save();

try {
if (password && service === this.authService) {
const value = JSON.parse(password);
if (Array.isArray(value) && value.length === 0) {
await this.logout(service);
}
}
} catch (error) {
console.log(error);
}
}

async deletePassword(service: string, account: string): Promise<boolean> {
const result = await this.doDeletePassword(service, account);

if (result && service === this.authService) {
try {
await this.logout(service);
} catch (error) {
console.log(error);
}
}

return result;
}

private async doDeletePassword(service: string, account: string): Promise<boolean> {
let found = false;

this._credentials = this.credentials.filter(credential => {
Expand Down Expand Up @@ -104,6 +174,16 @@ class LocalStorageCredentialsProvider implements ICredentialsProvider {
.filter(credential => credential.service === service)
.map(({ account, password }) => ({ account, password }));
}

private async logout(service: string): Promise<void> {
const queryValues: Map<string, string> = new Map();
queryValues.set('logout', String(true));
queryValues.set('service', service);

await request({
url: doCreateUri('/auth/logout', queryValues).toString(true)
}, CancellationToken.None);
}
}

class PollingURLCallbackProvider extends Disposable implements IURLCallbackProvider {
Expand Down Expand Up @@ -154,7 +234,7 @@ class PollingURLCallbackProvider extends Disposable implements IURLCallbackProvi
// Start to poll on the callback being fired
this.periodicFetchCallback(requestId, Date.now());

return this.doCreateUri('/callback', queryValues);
return doCreateUri('/callback', queryValues);
}

private async periodicFetchCallback(requestId: string, startTime: number): Promise<void> {
Expand All @@ -164,7 +244,7 @@ class PollingURLCallbackProvider extends Disposable implements IURLCallbackProvi
queryValues.set(PollingURLCallbackProvider.QUERY_KEYS.REQUEST_ID, requestId);

const result = await request({
url: this.doCreateUri('/fetch-callback', queryValues).toString(true)
url: doCreateUri('/fetch-callback', queryValues).toString(true)
}, CancellationToken.None);

// Check for callback results
Expand All @@ -185,23 +265,6 @@ class PollingURLCallbackProvider extends Disposable implements IURLCallbackProvi
}
}

private doCreateUri(path: string, queryValues: Map<string, string>): URI {
let query: string | undefined = undefined;

if (queryValues) {
let index = 0;
queryValues.forEach((value, key) => {
if (!query) {
query = '';
}

const prefix = (index++ === 0) ? '' : '&';
query += `${prefix}${key}=${encodeURIComponent(value)}`;
});
}

return URI.parse(window.location.href).with({ path, query });
}
}

class WorkspaceProvider implements IWorkspaceProvider {
Expand Down Expand Up @@ -430,21 +493,6 @@ class WindowIndicator implements IWindowIndicator {
window.location.href = `${window.location.origin}?${queryString}`;
};

// Credentials (with support of predefined ones via meta element)
const credentialsProvider = new LocalStorageCredentialsProvider();

const credentialsElement = document.getElementById('vscode-workbench-credentials');
const credentialsElementAttribute = credentialsElement ? credentialsElement.getAttribute('data-settings') : undefined;
let credentials = undefined;
if (credentialsElementAttribute) {
try {
credentials = JSON.parse(credentialsElementAttribute);
for (const { service, account, password } of credentials) {
credentialsProvider.setPassword(service, account, password);
}
} catch (error) { /* Invalid credentials are passed. Ignore. */ }
}

// Finally create workbench
create(document.body, {
...config,
Expand All @@ -453,6 +501,6 @@ class WindowIndicator implements IWindowIndicator {
productQualityChangeHandler,
workspaceProvider,
urlCallbackProvider: new PollingURLCallbackProvider(),
credentialsProvider
credentialsProvider: new LocalStorageCredentialsProvider()
});
})();
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,10 @@ export class AccountsActionViewItem extends ActivityActionViewItem {
return this.authenticationService.signOutOfAccount(sessionInfo.providerId, accountName);
});

const actions = hasEmbedderAccountSession ? [manageExtensionsAction] : [manageExtensionsAction, signOutAction];
const actions = [manageExtensionsAction];
if (!hasEmbedderAccountSession || authenticationSession?.canSignOut) {
actions.push(signOutAction);
}

const menu = new SubmenuAction('activitybar.submenu', `${accountName} (${providerDisplayName})`, actions);
menus.push(menu);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import { IExtensionService } from 'vs/workbench/services/extensions/common/exten

export function getAuthenticationProviderActivationEvent(id: string): string { return `onAuthenticationRequest:${id}`; }

export type AuthenticationSessionInfo = { readonly id: string, readonly accessToken: string, readonly providerId: string };
export type AuthenticationSessionInfo = { readonly id: string, readonly accessToken: string, readonly providerId: string, readonly canSignOut?: boolean };
export async function getCurrentAuthenticationSessionInfo(environmentService: IWorkbenchEnvironmentService, productService: IProductService): Promise<AuthenticationSessionInfo | undefined> {
if (environmentService.options?.credentialsProvider) {
const authenticationSessionValue = await environmentService.options.credentialsProvider.getPassword(`${productService.urlProtocol}.login`, 'account');
Expand Down

0 comments on commit 9e50567

Please sign in to comment.