Use our own GitHub auth relay server Microsoft's does not work with self-hosted instances so we run our own. Also add an extra set of scopes so that tokens provided via --github-auth will work for the PR extension. Index: code-server/lib/vscode/extensions/github-authentication/src/githubServer.ts =================================================================== --- code-server.orig/lib/vscode/extensions/github-authentication/src/githubServer.ts +++ code-server/lib/vscode/extensions/github-authentication/src/githubServer.ts @@ -17,7 +17,7 @@ const localize = nls.loadMessageBundle() const CLIENT_ID = '01ab8ac9400c4e429b23'; const NETWORK_ERROR = 'network error'; -const AUTH_RELAY_SERVER = 'vscode-auth.github.com'; +const AUTH_RELAY_SERVER = 'auth.code-server.dev'; // const AUTH_RELAY_STAGING_SERVER = 'client-auth-staging-14a768b.herokuapp.com'; class UriEventHandler extends vscode.EventEmitter implements vscode.UriHandler { Index: code-server/lib/vscode/src/vs/server/node/webClientServer.ts =================================================================== --- code-server.orig/lib/vscode/src/vs/server/node/webClientServer.ts +++ code-server/lib/vscode/src/vs/server/node/webClientServer.ts @@ -274,7 +274,7 @@ export class WebClientServer { id: generateUuid(), providerId: 'github', accessToken: this._environmentService.args['github-auth'], - scopes: [['user:email'], ['repo']] + scopes: [['read:user', 'user:email', 'repo'], ['user:email'], ['repo']] } : undefined; const base = relativeRoot(getOriginalUrl(req)) const vscodeBase = relativePath(getOriginalUrl(req)) Index: code-server/lib/vscode/src/vs/code/browser/workbench/workbench.ts =================================================================== --- code-server.orig/lib/vscode/src/vs/code/browser/workbench/workbench.ts +++ code-server/lib/vscode/src/vs/code/browser/workbench/workbench.ts @@ -17,6 +17,7 @@ import { isFolderToOpen, isWorkspaceToOp import { create, ICredentialsProvider, IURLCallbackProvider, IWorkbenchConstructionOptions, IWorkspace, IWorkspaceProvider } from 'vs/workbench/workbench.web.main'; import { posix } from 'vs/base/common/path'; import { ltrim } from 'vs/base/common/strings'; +import { equals as arrayEquals } from 'vs/base/common/arrays'; interface ICredential { service: string; @@ -24,6 +25,13 @@ interface ICredential { password: string; } +interface IToken { + accessToken: string + account?: { label: string } + id: string + scopes: string[] +} + class LocalStorageCredentialsProvider implements ICredentialsProvider { private static readonly CREDENTIALS_STORAGE_KEY = 'credentials.provider'; @@ -51,6 +59,58 @@ class LocalStorageCredentialsProvider im scopes, accessToken: authSessionInfo!.accessToken })))); + + // Add tokens for extensions to use. This works for extensions like the + // pull requests one or GitLens. + const extensionId = `vscode.${authSessionInfo.providerId}-authentication`; + const service = `${product.urlProtocol}${extensionId}`; + const account = `${authSessionInfo.providerId}.auth`; + // Oddly the scopes need to match exactly so we cannot just have one token + // with all the scopes, instead we have to duplicate the token for each + // expected set of scopes. + const tokens: IToken[] = authSessionInfo.scopes.map((scopes) => ({ + id: authSessionInfo!.id, + scopes: scopes.sort(), // Sort for comparing later. + accessToken: authSessionInfo!.accessToken, + })); + this.getPassword(service, account).then((raw) => { + let existing: { + content: IToken[] + } | undefined; + + if (raw) { + try { + const json = JSON.parse(raw); + json.content = JSON.parse(json.content); + existing = json; + } catch (error) { + console.log(error); + } + } + + // Keep tokens for account and scope combinations we do not have in case + // there is an extension that uses scopes we have not accounted for (in + // these cases the user will need to manually authenticate the extension + // through the UI) or the user has tokens for other accounts. + if (existing?.content) { + existing.content = existing.content.filter((existingToken) => { + const scopes = existingToken.scopes.sort(); + return !(tokens.find((token) => { + return arrayEquals(scopes, token.scopes) + && token.account?.label === existingToken.account?.label; + })) + }) + } + + return this.setPassword(service, account, JSON.stringify({ + extensionId, + ...(existing || {}), + content: JSON.stringify([ + ...tokens, + ...(existing?.content || []), + ]) + })); + }) } }