Skip to content

Commit 2fabac4

Browse files
Introduce auth-provider specific props & use it for social sign in for GitHub (microsoft#251649)
ref microsoft#251648 NOTE: the server side is not quite ready. It doesn't redirect back to VS Code properly, but this should be good to go whenever that is.
1 parent 480485f commit 2fabac4

File tree

12 files changed

+162
-28
lines changed

12 files changed

+162
-28
lines changed

extensions/github-authentication/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@
1818
"workspace"
1919
],
2020
"enabledApiProposals": [
21-
"authIssuers"
21+
"authIssuers",
22+
"authProviderSpecific"
2223
],
2324
"activationEvents": [],
2425
"capabilities": {

extensions/github-authentication/src/flows.ts

Lines changed: 65 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -60,15 +60,46 @@ export interface IFlowQuery {
6060
}
6161

6262
interface IFlowTriggerOptions {
63+
/**
64+
* The scopes to request for the OAuth flow.
65+
*/
6366
scopes: string;
67+
/**
68+
* The base URI for the flow. This is used to determine which GitHub instance to authenticate against.
69+
*/
6470
baseUri: Uri;
65-
logger: Log;
71+
/**
72+
* The specific auth provider to use for the flow.
73+
*/
74+
signInProvider?: GitHubSocialSignInProvider;
75+
/**
76+
* The Uri that the OAuth flow will redirect to. (i.e. vscode.dev/redirect)
77+
*/
6678
redirectUri: Uri;
67-
nonce: string;
79+
/**
80+
* The Uri to redirect to after redirecting to the redirect Uri. (i.e. vscode://....)
81+
*/
6882
callbackUri: Uri;
69-
uriHandler: UriEventHandler;
83+
/**
84+
* The enterprise URI for the flow, if applicable.
85+
*/
7086
enterpriseUri?: Uri;
87+
/**
88+
* The existing login which will be used to pre-fill the login prompt.
89+
*/
7190
existingLogin?: string;
91+
/**
92+
* The nonce for this particular flow. This is used to prevent replay attacks.
93+
*/
94+
nonce: string;
95+
/**
96+
* The instance of the Uri Handler for this extension
97+
*/
98+
uriHandler: UriEventHandler;
99+
/**
100+
* The logger to use for this flow.
101+
*/
102+
logger: Log;
72103
}
73104

74105
interface IFlow {
@@ -143,12 +174,13 @@ class UrlHandlerFlow implements IFlow {
143174
scopes,
144175
baseUri,
145176
redirectUri,
146-
logger,
147-
nonce,
148177
callbackUri,
149-
uriHandler,
150178
enterpriseUri,
151-
existingLogin
179+
nonce,
180+
signInProvider: authProvider,
181+
uriHandler,
182+
existingLogin,
183+
logger,
152184
}: IFlowTriggerOptions): Promise<string> {
153185
logger.info(`Trying without local server... (${scopes})`);
154186
return await window.withProgress<string>({
@@ -177,7 +209,7 @@ class UrlHandlerFlow implements IFlow {
177209
// The extra toString, parse is apparently needed for env.openExternal
178210
// to open the correct URL.
179211
const uri = Uri.parse(baseUri.with({
180-
path: '/login/oauth/authorize',
212+
path: getAuthorizeUrlPath(authProvider),
181213
query: searchParams.toString()
182214
}).toString(true));
183215
await env.openExternal(uri);
@@ -219,10 +251,11 @@ class LocalServerFlow implements IFlow {
219251
scopes,
220252
baseUri,
221253
redirectUri,
222-
logger,
223254
callbackUri,
224255
enterpriseUri,
225-
existingLogin
256+
signInProvider: authProvider,
257+
existingLogin,
258+
logger
226259
}: IFlowTriggerOptions): Promise<string> {
227260
logger.info(`Trying with local server... (${scopes})`);
228261
return await window.withProgress<string>({
@@ -246,7 +279,7 @@ class LocalServerFlow implements IFlow {
246279
}
247280

248281
const loginUrl = baseUri.with({
249-
path: '/login/oauth/authorize',
282+
path: getAuthorizeUrlPath(authProvider),
250283
query: searchParams.toString()
251284
});
252285
const server = new LoopbackAuthServer(path.join(__dirname, '../media'), loginUrl.toString(true), callbackUri.toString(true));
@@ -520,3 +553,24 @@ export function getFlows(query: IFlowQuery) {
520553
return useFlow;
521554
});
522555
}
556+
557+
/**
558+
* Social authentication providers for GitHub
559+
*/
560+
export const enum GitHubSocialSignInProvider {
561+
Google = 'google',
562+
// Apple = 'apple',
563+
}
564+
565+
export function isSocialSignInProvider(provider: unknown): provider is GitHubSocialSignInProvider {
566+
return provider === GitHubSocialSignInProvider.Google; // || provider === GitHubSocialSignInProvider.Apple;
567+
}
568+
569+
export function getAuthorizeUrlPath(provider: GitHubSocialSignInProvider | undefined): string {
570+
switch (provider) {
571+
case GitHubSocialSignInProvider.Google:
572+
// case GitHubSocialSignInProvider.Apple:
573+
return `/sessions/social/${provider}/initiate`;
574+
}
575+
return '/login/oauth/authorize';
576+
}

extensions/github-authentication/src/github.ts

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { ExperimentationTelemetry } from './common/experimentationService';
1212
import { Log } from './common/logger';
1313
import { crypto } from './node/crypto';
1414
import { TIMED_OUT_ERROR, USER_CANCELLATION_ERROR } from './common/errors';
15+
import { GitHubSocialSignInProvider, isSocialSignInProvider } from './flows';
1516

1617
interface SessionData {
1718
id: string;
@@ -31,6 +32,20 @@ export enum AuthProviderType {
3132
githubEnterprise = 'github-enterprise'
3233
}
3334

35+
interface GitHubAuthenticationProviderOptions extends vscode.AuthenticationProviderSessionOptions {
36+
/**
37+
* This is specific to GitHub and is used to determine which social sign-in provider to use.
38+
* If not provided, the default (GitHub) is used which shows all options.
39+
*
40+
* Example: If you specify Google, then the sign-in flow will skip the initial page that asks you
41+
* to choose how you want to sign in and will directly take you to the Google sign-in page.
42+
*
43+
* This allows us to show "Continue with Google" buttons in the product, rather than always
44+
* leaving it up to the user to choose the social sign-in provider on the sign-in page.
45+
*/
46+
readonly provider?: GitHubSocialSignInProvider;
47+
}
48+
3449
export class UriEventHandler extends vscode.EventEmitter<vscode.Uri> implements vscode.UriHandler {
3550
private readonly _pendingNonces = new Map<string, string[]>();
3651
private readonly _codeExchangePromises = new Map<string, { promise: Promise<string>; cancel: vscode.EventEmitter<void> }>();
@@ -306,7 +321,7 @@ export class GitHubAuthenticationProvider implements vscode.AuthenticationProvid
306321
this._logger.info(`Stored ${sessions.length} sessions!`);
307322
}
308323

309-
public async createSession(scopes: string[], options?: vscode.AuthenticationProviderSessionOptions): Promise<vscode.AuthenticationSession> {
324+
public async createSession(scopes: string[], options?: GitHubAuthenticationProviderOptions): Promise<vscode.AuthenticationSession> {
310325
try {
311326
// For GitHub scope list, order doesn't matter so we use a sorted scope to determine
312327
// if we've got a session already.
@@ -325,9 +340,10 @@ export class GitHubAuthenticationProvider implements vscode.AuthenticationProvid
325340

326341
const sessions = await this._sessionsPromise;
327342
const loginWith = options?.account?.label;
328-
this._logger.info(`Logging in with '${loginWith ? loginWith : 'any'}' account...`);
343+
const signInProvider = isSocialSignInProvider(options?.provider) ? options.provider : undefined;
344+
this._logger.info(`Logging in with${signInProvider ? ` ${signInProvider}, ` : ''} '${loginWith ? loginWith : 'any'}' account...`);
329345
const scopeString = sortedScopes.join(' ');
330-
const token = await this._githubServer.login(scopeString, loginWith);
346+
const token = await this._githubServer.login(scopeString, signInProvider, loginWith);
331347
const session = await this.tokenToSession(token, scopes);
332348
this.afterSessionLoad(session);
333349

extensions/github-authentication/src/githubServer.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import { Log } from './common/logger';
1010
import { isSupportedClient, isSupportedTarget } from './common/env';
1111
import { crypto } from './node/crypto';
1212
import { fetching } from './node/fetch';
13-
import { ExtensionHost, GitHubTarget, getFlows } from './flows';
13+
import { ExtensionHost, GitHubSocialSignInProvider, GitHubTarget, getFlows } from './flows';
1414
import { CANCELLATION_ERROR, NETWORK_ERROR, USER_CANCELLATION_ERROR } from './common/errors';
1515
import { Config } from './config';
1616
import { base64Encode } from './node/buffer';
@@ -19,7 +19,7 @@ const REDIRECT_URL_STABLE = 'https://vscode.dev/redirect';
1919
const REDIRECT_URL_INSIDERS = 'https://insiders.vscode.dev/redirect';
2020

2121
export interface IGitHubServer {
22-
login(scopes: string, existingLogin?: string): Promise<string>;
22+
login(scopes: string, signInProvider?: GitHubSocialSignInProvider, existingLogin?: string): Promise<string>;
2323
logout(session: vscode.AuthenticationSession): Promise<void>;
2424
getUserInfo(token: string): Promise<{ id: string; accountName: string }>;
2525
sendAdditionalTelemetryInfo(session: vscode.AuthenticationSession): Promise<void>;
@@ -87,7 +87,7 @@ export class GitHubServer implements IGitHubServer {
8787
return this._isNoCorsEnvironment;
8888
}
8989

90-
public async login(scopes: string, existingLogin?: string): Promise<string> {
90+
public async login(scopes: string, signInProvider?: GitHubSocialSignInProvider, existingLogin?: string): Promise<string> {
9191
this._logger.info(`Logging in for the following scopes: ${scopes}`);
9292

9393
// Used for showing a friendlier message to the user when the explicitly cancel a flow.
@@ -135,6 +135,7 @@ export class GitHubServer implements IGitHubServer {
135135
scopes,
136136
callbackUri,
137137
nonce,
138+
signInProvider,
138139
baseUri: this.baseUri,
139140
logger: this._logger,
140141
uriHandler: this._uriHandler,

extensions/github-authentication/tsconfig.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
"include": [
1414
"src/**/*",
1515
"../../src/vscode-dts/vscode.d.ts",
16-
"../../src/vscode-dts/vscode.proposed.authIssuers.d.ts"
16+
"../../src/vscode-dts/vscode.proposed.authIssuers.d.ts",
17+
"../../src/vscode-dts/vscode.proposed.authProviderSpecific.d.ts"
1718
]
1819
}

src/vs/platform/extensions/common/extensionsApiProposals.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,9 @@ const _allApiProposals = {
2525
authLearnMore: {
2626
proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.authLearnMore.d.ts',
2727
},
28+
authProviderSpecific: {
29+
proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.authProviderSpecific.d.ts',
30+
},
2831
authSession: {
2932
proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.authSession.d.ts',
3033
},

src/vs/workbench/api/browser/mainThreadAuthentication.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
import { Disposable, DisposableMap } from '../../../base/common/lifecycle.js';
77
import * as nls from '../../../nls.js';
88
import { extHostNamedCustomer, IExtHostContext } from '../../services/extensions/common/extHostCustomers.js';
9-
import { IAuthenticationCreateSessionOptions, AuthenticationSession, AuthenticationSessionsChangeEvent, IAuthenticationProvider, IAuthenticationService, IAuthenticationExtensionsService, INTERNAL_AUTH_PROVIDER_PREFIX as INTERNAL_MODEL_AUTH_PROVIDER_PREFIX, AuthenticationSessionAccount, IAuthenticationProviderSessionOptions } from '../../services/authentication/common/authentication.js';
9+
import { AuthenticationSession, AuthenticationSessionsChangeEvent, IAuthenticationProvider, IAuthenticationService, IAuthenticationExtensionsService, INTERNAL_AUTH_PROVIDER_PREFIX as INTERNAL_MODEL_AUTH_PROVIDER_PREFIX, AuthenticationSessionAccount, IAuthenticationProviderSessionOptions } from '../../services/authentication/common/authentication.js';
1010
import { ExtHostAuthenticationShape, ExtHostContext, MainContext, MainThreadAuthenticationShape } from '../common/extHost.protocol.js';
1111
import { IDialogService, IPromptButton } from '../../../platform/dialogs/common/dialogs.js';
1212
import Severity from '../../../base/common/severity.js';
@@ -64,7 +64,7 @@ export class MainThreadAuthenticationProvider extends Disposable implements IAut
6464
return this._proxy.$getSessions(this.id, scopes, options);
6565
}
6666

67-
createSession(scopes: string[], options: IAuthenticationCreateSessionOptions): Promise<AuthenticationSession> {
67+
createSession(scopes: string[], options: IAuthenticationProviderSessionOptions): Promise<AuthenticationSession> {
6868
return this._proxy.$createSession(this.id, scopes, options);
6969
}
7070

src/vs/workbench/api/common/extHost.protocol.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ import { Timeline, TimelineChangeEvent, TimelineOptions, TimelineProviderDescrip
7878
import { TypeHierarchyItem } from '../../contrib/typeHierarchy/common/typeHierarchy.js';
7979
import { RelatedInformationResult, RelatedInformationType } from '../../services/aiRelatedInformation/common/aiRelatedInformation.js';
8080
import { AiSettingsSearchProviderOptions, AiSettingsSearchResult } from '../../services/aiSettingsSearch/common/aiSettingsSearch.js';
81-
import { AuthenticationSession, AuthenticationSessionAccount, AuthenticationSessionsChangeEvent, IAuthenticationCreateSessionOptions, IAuthenticationProviderSessionOptions } from '../../services/authentication/common/authentication.js';
81+
import { AuthenticationSession, AuthenticationSessionAccount, AuthenticationSessionsChangeEvent, IAuthenticationCreateSessionOptions, IAuthenticationGetSessionsOptions } from '../../services/authentication/common/authentication.js';
8282
import { EditorGroupColumn } from '../../services/editor/common/editorGroupColumn.js';
8383
import { IExtensionDescriptionDelta, IStaticWorkspaceData } from '../../services/extensions/common/extensionHostProtocol.js';
8484
import { IResolveAuthorityResult } from '../../services/extensions/common/extensionHostProxy.js';
@@ -1981,7 +1981,7 @@ export interface ExtHostLabelServiceShape {
19811981
}
19821982

19831983
export interface ExtHostAuthenticationShape {
1984-
$getSessions(id: string, scopes: string[] | undefined, options: IAuthenticationProviderSessionOptions): Promise<ReadonlyArray<AuthenticationSession>>;
1984+
$getSessions(id: string, scopes: string[] | undefined, options: IAuthenticationGetSessionsOptions): Promise<ReadonlyArray<AuthenticationSession>>;
19851985
$createSession(id: string, scopes: string[], options: IAuthenticationCreateSessionOptions): Promise<AuthenticationSession>;
19861986
$removeSession(id: string, sessionId: string): Promise<void>;
19871987
$onDidChangeAuthenticationSessions(id: string, label: string, extensionIdFilter?: string[]): Promise<void>;

src/vs/workbench/contrib/chat/common/chatEntitlementService.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -847,6 +847,7 @@ export class ChatEntitlementRequests extends Disposable {
847847

848848
async signIn() {
849849
const providerId = ChatEntitlementRequests.providerId(this.configurationService);
850+
// TODO: Pass in { provider: 'google' } for google sign-in
850851
const session = await this.authenticationService.createSession(providerId, defaultChat.providerScopes[0]);
851852

852853
this.authenticationExtensionsService.updateAccountPreference(defaultChat.extensionId, providerId, session.account);

src/vs/workbench/services/authentication/browser/authenticationService.ts

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -281,7 +281,7 @@ export class AuthenticationService extends Disposable implements IAuthentication
281281
throw new Error(`The authorization server '${authServerStr}' is not supported by the authentication provider '${id}'.`);
282282
}
283283
}
284-
return await authProvider.getSessions(scopes, { account: options?.account, authorizationServer: options?.authorizationServer });
284+
return await authProvider.getSessions(scopes, { ...options });
285285
} else {
286286
throw new Error(`No authentication provider '${id}' is currently registered.`);
287287
}
@@ -294,10 +294,7 @@ export class AuthenticationService extends Disposable implements IAuthentication
294294

295295
const authProvider = this._authenticationProviders.get(id) || await this.tryActivateProvider(id, !!options?.activateImmediate);
296296
if (authProvider) {
297-
return await authProvider.createSession(scopes, {
298-
account: options?.account,
299-
authorizationServer: options?.authorizationServer
300-
});
297+
return await authProvider.createSession(scopes, { ...options });
301298
} else {
302299
throw new Error(`No authentication provider '${id}' is currently registered.`);
303300
}

0 commit comments

Comments
 (0)