1
- import * as vscode from 'vscode' ;
2
- import { IHostConfiguration , HostHelper } from './configuration' ;
3
1
import * as https from 'https' ;
4
- import { Base64 } from 'js-base64' ;
5
- import { parse } from 'query-string' ;
2
+ import * as vscode from 'vscode' ;
6
3
import Logger from '../common/logger' ;
4
+ import { agent } from '../common/net' ;
7
5
import { handler as uriHandler } from '../common/uri' ;
8
6
import { 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' ) ;
12
10
13
11
const SCOPES : string = 'read:user user:email repo write:discussion' ;
14
12
const GHE_OPTIONAL_SCOPES : { [ key : string ] : boolean } = { 'write:discussion' : true } ;
15
13
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' ;
20
15
21
16
export class GitHubManager {
22
17
private _servers : Map < string , boolean > = new Map ( ) . set ( 'github.com' , true ) ;
@@ -124,40 +119,54 @@ export class GitHubManager {
124
119
}
125
120
}
126
121
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 ;
130
126
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 ;
157
131
}
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
+ } ) ;
159
160
} ;
160
161
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
+
161
170
const manuallyEnteredToken : ( host : string ) => PromiseAdapter < IHostConfiguration , IHostConfiguration > =
162
171
host => ( config : IHostConfiguration , resolve ) =>
163
172
config . host === toCanonical ( host ) && resolve ( config ) ;
@@ -172,14 +181,15 @@ export class GitHubServer {
172
181
this . hostUri = vscode . Uri . parse ( host ) ;
173
182
}
174
183
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` ) ;
176
188
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 ) ;
181
191
return Promise . race ( [
182
- promiseFromEvent ( uriHandler . event , verifyToken ( host ) ) ,
192
+ promiseFromEvent ( uriHandler . event , exchangeCodeForToken ( host , state ) ) ,
183
193
promiseFromEvent ( onKeychainDidChange , manuallyEnteredToken ( host ) )
184
194
] ) ;
185
195
}
0 commit comments