1- import * as vscode from 'vscode' ;
2- import { IHostConfiguration , HostHelper } from './configuration' ;
31import * as https from 'https' ;
4- import { Base64 } from 'js-base64' ;
5- import { parse } from 'query-string' ;
2+ import * as vscode from 'vscode' ;
63import Logger from '../common/logger' ;
4+ import { agent } from '../common/net' ;
75import { handler as uriHandler } from '../common/uri' ;
86import { PromiseAdapter , promiseFromEvent } from '../common/utils' ;
9- import { agent } from '../common/net ' ;
10- import { EXTENSION_ID } from '../constants ' ;
11- import { onDidChange as onKeychainDidChange , toCanonical , listHosts } from './keychain' ;
7+ import { HostHelper , IHostConfiguration } from './configuration ' ;
8+ import { listHosts , onDidChange as onKeychainDidChange , toCanonical } from './keychain ' ;
9+ import uuid = require ( 'uuid' ) ;
1210
1311const SCOPES : string = 'read:user user:email repo write:discussion' ;
1412const GHE_OPTIONAL_SCOPES : { [ key : string ] : boolean } = { 'write:discussion' : true } ;
1513
16- const AUTH_RELAY_SERVER = 'https://vscode-auth.github.com' ;
17- const CALLBACK_PATH = '/did-authenticate' ;
18- const CALLBACK_URI = `${ vscode . env . uriScheme } ://${ EXTENSION_ID } ${ CALLBACK_PATH } ` ;
19- const MAX_TOKEN_RESPONSE_AGE = 5 * ( 1000 * 60 /* minutes in ms */ ) ;
14+ const AUTH_RELAY_SERVER = 'vscode-auth.github.com' ;
2015
2116export class GitHubManager {
2217 private _servers : Map < string , boolean > = new Map ( ) . set ( 'github.com' , true ) ;
@@ -124,40 +119,54 @@ export class GitHubManager {
124119 }
125120}
126121
127- class ResponseExpired extends Error {
128- get message ( ) { return 'Token response expired' ; }
129- }
122+ const exchangeCodeForToken : ( host : string , state : string ) => PromiseAdapter < vscode . Uri , IHostConfiguration > =
123+ ( host , state ) => async ( uri , resolve , reject ) => {
124+ const query = parseQuery ( uri ) ;
125+ const code = query . code ;
130126
131- const SEPARATOR = '/' , SEPARATOR_LEN = SEPARATOR . length ;
132-
133- /**
134- * Hydrate and verify the signature of a message produced with `encode`
135- *
136- * Returns an object
137- *
138- * @param {string } signedMessage signed message produced by encode
139- * @returns {any } decoded JSON data
140- * @throws {SyntaxError } if the message was null or could not be parsed as JSON
141- */
142- const decode = ( signedMessage ?: string ) : any => {
143- if ( ! signedMessage ) { throw new SyntaxError ( 'Invalid encoding' ) ; }
144- const separatorIndex = signedMessage . indexOf ( SEPARATOR ) ;
145- const message = signedMessage . substr ( separatorIndex + SEPARATOR_LEN ) ;
146- return JSON . parse ( Base64 . decode ( message ) ) ;
147- } ;
148-
149- const verifyToken : ( host : string ) => PromiseAdapter < vscode . Uri , IHostConfiguration > =
150- host => async ( uri , resolve , reject ) => {
151- if ( uri . path !== CALLBACK_PATH ) { return ; }
152- const query = parse ( uri . query ) ;
153- const state = decode ( query . state as string ) ;
154- const { ts, access_token : token } = state . token ;
155- if ( Date . now ( ) - ts > MAX_TOKEN_RESPONSE_AGE ) {
156- return reject ( new ResponseExpired ) ;
127+ if ( query . state !== state ) {
128+ vscode . window . showInformationMessage ( 'Sign in failed: Received bad state' ) ;
129+ reject ( 'Received bad state' ) ;
130+ return ;
157131 }
158- resolve ( { host, token } ) ;
132+
133+ const post = https . request ( {
134+ host : AUTH_RELAY_SERVER ,
135+ path : `/token?code=${ code } &state=${ query . state } ` ,
136+ method : 'POST' ,
137+ headers : {
138+ Accept : 'application/json'
139+ }
140+ } , result => {
141+ const buffer : Buffer [ ] = [ ] ;
142+ result . on ( 'data' , ( chunk : Buffer ) => {
143+ buffer . push ( chunk ) ;
144+ } ) ;
145+ result . on ( 'end' , ( ) => {
146+ if ( result . statusCode === 200 ) {
147+ const json = JSON . parse ( Buffer . concat ( buffer ) . toString ( ) ) ;
148+ resolve ( { host, token : json . access_token } ) ;
149+ } else {
150+ vscode . window . showInformationMessage ( `Sign in failed: ${ result . statusMessage } ` ) ;
151+ reject ( new Error ( result . statusMessage ) ) ;
152+ }
153+ } ) ;
154+ } ) ;
155+
156+ post . end ( ) ;
157+ post . on ( 'error' , err => {
158+ reject ( err ) ;
159+ } ) ;
159160 } ;
160161
162+ function parseQuery ( uri : vscode . Uri ) {
163+ return uri . query . split ( '&' ) . reduce ( ( prev : any , current ) => {
164+ const queryString = current . split ( '=' ) ;
165+ prev [ queryString [ 0 ] ] = queryString [ 1 ] ;
166+ return prev ;
167+ } , { } ) ;
168+ }
169+
161170const manuallyEnteredToken : ( host : string ) => PromiseAdapter < IHostConfiguration , IHostConfiguration > =
162171 host => ( config : IHostConfiguration , resolve ) =>
163172 config . host === toCanonical ( host ) && resolve ( config ) ;
@@ -172,14 +181,15 @@ export class GitHubServer {
172181 this . hostUri = vscode . Uri . parse ( host ) ;
173182 }
174183
175- public login ( ) : Promise < IHostConfiguration > {
184+ public async login ( ) : Promise < IHostConfiguration > {
185+ const state = uuid ( ) ;
186+ const callbackUri = await vscode . env . createAppUri ( { payload : { path : '/did-authenticate' } } ) ;
187+ const uri = vscode . Uri . parse ( `https://${ AUTH_RELAY_SERVER } /authorize/?callbackUri=${ encodeURIComponent ( callbackUri . toString ( ) ) } &scope=${ SCOPES } &state=${ state } &responseType=code` ) ;
176188 const host = this . hostUri . toString ( ) ;
177- const uri = vscode . Uri . parse (
178- `${ AUTH_RELAY_SERVER } /authorize?authServer=${ host } &callbackUri=${ CALLBACK_URI } &scope=${ SCOPES } `
179- ) ;
180- vscode . commands . executeCommand ( 'vscode.open' , uri ) ;
189+
190+ vscode . env . openExternal ( uri ) ;
181191 return Promise . race ( [
182- promiseFromEvent ( uriHandler . event , verifyToken ( host ) ) ,
192+ promiseFromEvent ( uriHandler . event , exchangeCodeForToken ( host , state ) ) ,
183193 promiseFromEvent ( onKeychainDidChange , manuallyEnteredToken ( host ) )
184194 ] ) ;
185195 }
0 commit comments