Wire in a auth callback parameter to indicate Copilot terms were shown to the user (microsoft/vscode-internalbacklog#5761) (#262439)

* implement as a bag

* nit

---------

Co-authored-by: TylerLeonhardt <2644648+TylerLeonhardt@users.noreply.github.com>
This commit is contained in:
Benjamin Pasero
2025-08-22 07:42:25 +02:00
committed by GitHub
parent 8c4dda46cd
commit d44d16c464
4 changed files with 61 additions and 7 deletions

View File

@@ -72,6 +72,10 @@ interface IFlowTriggerOptions {
* The specific auth provider to use for the flow. * The specific auth provider to use for the flow.
*/ */
signInProvider?: GitHubSocialSignInProvider; signInProvider?: GitHubSocialSignInProvider;
/**
* Extra parameters to include in the OAuth flow.
*/
extraAuthorizeParameters?: Record<string, string>;
/** /**
* The Uri that the OAuth flow will redirect to. (i.e. vscode.dev/redirect) * The Uri that the OAuth flow will redirect to. (i.e. vscode.dev/redirect)
*/ */
@@ -180,6 +184,7 @@ class UrlHandlerFlow implements IFlow {
enterpriseUri, enterpriseUri,
nonce, nonce,
signInProvider, signInProvider,
extraAuthorizeParameters,
uriHandler, uriHandler,
existingLogin, existingLogin,
logger, logger,
@@ -210,6 +215,11 @@ class UrlHandlerFlow implements IFlow {
if (signInProvider) { if (signInProvider) {
searchParams.append('provider', signInProvider); searchParams.append('provider', signInProvider);
} }
if (extraAuthorizeParameters) {
for (const [key, value] of Object.entries(extraAuthorizeParameters)) {
searchParams.append(key, value);
}
}
// The extra toString, parse is apparently needed for env.openExternal // The extra toString, parse is apparently needed for env.openExternal
// to open the correct URL. // to open the correct URL.
@@ -259,6 +269,7 @@ class LocalServerFlow implements IFlow {
callbackUri, callbackUri,
enterpriseUri, enterpriseUri,
signInProvider, signInProvider,
extraAuthorizeParameters,
existingLogin, existingLogin,
logger logger
}: IFlowTriggerOptions): Promise<string> { }: IFlowTriggerOptions): Promise<string> {
@@ -285,6 +296,11 @@ class LocalServerFlow implements IFlow {
if (signInProvider) { if (signInProvider) {
searchParams.append('provider', signInProvider); searchParams.append('provider', signInProvider);
} }
if (extraAuthorizeParameters) {
for (const [key, value] of Object.entries(extraAuthorizeParameters)) {
searchParams.append(key, value);
}
}
const loginUrl = baseUri.with({ const loginUrl = baseUri.with({
path: '/login/oauth/authorize', path: '/login/oauth/authorize',
@@ -332,7 +348,7 @@ class DeviceCodeFlow implements IFlow {
supportsSupportedClients: true, supportsSupportedClients: true,
supportsUnsupportedClients: true supportsUnsupportedClients: true
}; };
async trigger({ scopes, baseUri, signInProvider, logger }: IFlowTriggerOptions) { async trigger({ scopes, baseUri, signInProvider, extraAuthorizeParameters, logger }: IFlowTriggerOptions) {
logger.info(`Trying device code flow... (${scopes})`); logger.info(`Trying device code flow... (${scopes})`);
// Get initial device code // Get initial device code
@@ -369,9 +385,16 @@ class DeviceCodeFlow implements IFlow {
await env.clipboard.writeText(json.user_code); await env.clipboard.writeText(json.user_code);
let open = Uri.parse(json.verification_uri); let open = Uri.parse(json.verification_uri);
if (signInProvider) {
const query = new URLSearchParams(open.query); const query = new URLSearchParams(open.query);
if (signInProvider) {
query.set('provider', signInProvider); query.set('provider', signInProvider);
}
if (extraAuthorizeParameters) {
for (const [key, value] of Object.entries(extraAuthorizeParameters)) {
query.set(key, value);
}
}
if (signInProvider || extraAuthorizeParameters) {
open = open.with({ query: query.toString() }); open = open.with({ query: query.toString() });
} }
const uriToOpen = await env.asExternalUri(open); const uriToOpen = await env.asExternalUri(open);

View File

@@ -44,6 +44,27 @@ interface GitHubAuthenticationProviderOptions extends vscode.AuthenticationProvi
* leaving it up to the user to choose the social sign-in provider on the sign-in page. * leaving it up to the user to choose the social sign-in provider on the sign-in page.
*/ */
readonly provider?: GitHubSocialSignInProvider; readonly provider?: GitHubSocialSignInProvider;
readonly extraAuthorizeParameters?: Record<string, string>;
}
function isGitHubAuthenticationProviderOptions(object: any): object is GitHubAuthenticationProviderOptions {
if (!object || typeof object !== 'object') {
throw new Error('Options are not an object');
}
if (object.provider !== undefined && !isSocialSignInProvider(object.provider)) {
throw new Error(`Provider is invalid: ${object.provider}`);
}
if (object.extraAuthorizeParameters !== undefined) {
if (!object.extraAuthorizeParameters || typeof object.extraAuthorizeParameters !== 'object') {
throw new Error('Extra parameters must be a record of string keys and string values.');
}
for (const [key, value] of Object.entries(object.extraAuthorizeParameters)) {
if (typeof key !== 'string' || typeof value !== 'string') {
throw new Error('Extra parameters must be a record of string keys and string values.');
}
}
}
return true;
} }
export class UriEventHandler extends vscode.EventEmitter<vscode.Uri> implements vscode.UriHandler { export class UriEventHandler extends vscode.EventEmitter<vscode.Uri> implements vscode.UriHandler {
@@ -338,12 +359,15 @@ export class GitHubAuthenticationProvider implements vscode.AuthenticationProvid
scopes: JSON.stringify(scopes), scopes: JSON.stringify(scopes),
}); });
if (options && !isGitHubAuthenticationProviderOptions(options)) {
throw new Error('Invalid options');
}
const sessions = await this._sessionsPromise; const sessions = await this._sessionsPromise;
const loginWith = options?.account?.label; const loginWith = options?.account?.label;
const signInProvider = isSocialSignInProvider(options?.provider) ? options.provider : undefined; const signInProvider = options?.provider;
this._logger.info(`Logging in with${signInProvider ? ` ${signInProvider}, ` : ''} '${loginWith ? loginWith : 'any'}' account...`); this._logger.info(`Logging in with${signInProvider ? ` ${signInProvider}, ` : ''} '${loginWith ? loginWith : 'any'}' account...`);
const scopeString = sortedScopes.join(' '); const scopeString = sortedScopes.join(' ');
const token = await this._githubServer.login(scopeString, signInProvider, loginWith); const token = await this._githubServer.login(scopeString, signInProvider, options?.extraAuthorizeParameters, loginWith);
const session = await this.tokenToSession(token, scopes); const session = await this.tokenToSession(token, scopes);
this.afterSessionLoad(session); this.afterSessionLoad(session);

View File

@@ -19,7 +19,7 @@ const REDIRECT_URL_STABLE = 'https://vscode.dev/redirect';
const REDIRECT_URL_INSIDERS = 'https://insiders.vscode.dev/redirect'; const REDIRECT_URL_INSIDERS = 'https://insiders.vscode.dev/redirect';
export interface IGitHubServer { export interface IGitHubServer {
login(scopes: string, signInProvider?: GitHubSocialSignInProvider, existingLogin?: string): Promise<string>; login(scopes: string, signInProvider?: GitHubSocialSignInProvider, extraAuthorizeParameters?: Record<string, string>, existingLogin?: string): Promise<string>;
logout(session: vscode.AuthenticationSession): Promise<void>; logout(session: vscode.AuthenticationSession): Promise<void>;
getUserInfo(token: string): Promise<{ id: string; accountName: string }>; getUserInfo(token: string): Promise<{ id: string; accountName: string }>;
sendAdditionalTelemetryInfo(session: vscode.AuthenticationSession): Promise<void>; sendAdditionalTelemetryInfo(session: vscode.AuthenticationSession): Promise<void>;
@@ -87,7 +87,7 @@ export class GitHubServer implements IGitHubServer {
return this._isNoCorsEnvironment; return this._isNoCorsEnvironment;
} }
public async login(scopes: string, signInProvider?: GitHubSocialSignInProvider, existingLogin?: string): Promise<string> { public async login(scopes: string, signInProvider?: GitHubSocialSignInProvider, extraAuthorizeParameters?: Record<string, string>, existingLogin?: string): Promise<string> {
this._logger.info(`Logging in for the following scopes: ${scopes}`); this._logger.info(`Logging in for the following scopes: ${scopes}`);
// Used for showing a friendlier message to the user when the explicitly cancel a flow. // Used for showing a friendlier message to the user when the explicitly cancel a flow.
@@ -136,6 +136,7 @@ export class GitHubServer implements IGitHubServer {
callbackUri, callbackUri,
nonce, nonce,
signInProvider, signInProvider,
extraAuthorizeParameters,
baseUri: this.baseUri, baseUri: this.baseUri,
logger: this._logger, logger: this._logger,
uriHandler: this._uriHandler, uriHandler: this._uriHandler,

View File

@@ -908,7 +908,13 @@ export class ChatEntitlementRequests extends Disposable {
const providerId = ChatEntitlementRequests.providerId(this.configurationService); const providerId = ChatEntitlementRequests.providerId(this.configurationService);
const scopes = options?.additionalScopes ? distinct([...defaultChat.providerScopes[0], ...options.additionalScopes]) : defaultChat.providerScopes[0]; const scopes = options?.additionalScopes ? distinct([...defaultChat.providerScopes[0], ...options.additionalScopes]) : defaultChat.providerScopes[0];
const session = await this.authenticationService.createSession(providerId, scopes, options?.useSocialProvider ? { provider: options.useSocialProvider } : undefined); const session = await this.authenticationService.createSession(
providerId,
scopes,
{
extraAuthorizeParameters: { get_started_with: 'copilot-vscode' },
provider: options?.useSocialProvider
});
this.authenticationExtensionsService.updateAccountPreference(defaultChat.extensionId, providerId, session.account); this.authenticationExtensionsService.updateAccountPreference(defaultChat.extensionId, providerId, session.account);
this.authenticationExtensionsService.updateAccountPreference(defaultChat.chatExtensionId, providerId, session.account); this.authenticationExtensionsService.updateAccountPreference(defaultChat.chatExtensionId, providerId, session.account);