@@ -11,8 +11,10 @@ import { PromiseAdapter, promiseFromEvent } from './common/utils';
11
11
import { ExperimentationTelemetry } from './experimentationService' ;
12
12
import { AuthProviderType } from './github' ;
13
13
import { Log } from './common/logger' ;
14
+ import { isSupportedEnvironment } from './common/env' ;
14
15
15
16
const localize = nls . loadMessageBundle ( ) ;
17
+ const CLIENT_ID = '01ab8ac9400c4e429b23' ;
16
18
17
19
const NETWORK_ERROR = 'network error' ;
18
20
const AUTH_RELAY_SERVER = 'vscode-auth.github.com' ;
@@ -45,6 +47,13 @@ export interface IGitHubServer extends vscode.Disposable {
45
47
type : AuthProviderType ;
46
48
}
47
49
50
+ interface IGitHubDeviceCodeResponse {
51
+ device_code : string ;
52
+ user_code : string ;
53
+ verification_uri : string ;
54
+ interval : number ;
55
+ }
56
+
48
57
async function getScopes ( token : string , serverUri : vscode . Uri , logger : Log ) : Promise < string [ ] > {
49
58
try {
50
59
logger . info ( 'Getting token scopes...' ) ;
@@ -105,7 +114,7 @@ export class GitHubServer implements IGitHubServer {
105
114
private _disposable : vscode . Disposable ;
106
115
private _uriHandler = new UriEventHandler ( this . _logger ) ;
107
116
108
- constructor ( private readonly _logger : Log , private readonly _telemetryReporter : ExperimentationTelemetry ) {
117
+ constructor ( private readonly _supportDeviceCodeFlow : boolean , private readonly _logger : Log , private readonly _telemetryReporter : ExperimentationTelemetry ) {
109
118
this . _disposable = vscode . Disposable . from (
110
119
vscode . commands . registerCommand ( this . _statusBarCommandId , ( ) => this . manuallyProvideUri ( ) ) ,
111
120
vscode . window . registerUriHandler ( this . _uriHandler ) ) ;
@@ -115,10 +124,6 @@ export class GitHubServer implements IGitHubServer {
115
124
this . _disposable . dispose ( ) ;
116
125
}
117
126
118
- private isTestEnvironment ( url : vscode . Uri ) : boolean {
119
- return / \. a z u r e w e b s i t e s \. n e t $ / . test ( url . authority ) || url . authority . startsWith ( 'localhost:' ) ;
120
- }
121
-
122
127
// TODO@joaomoreno TODO@TylerLeonhardt
123
128
private async isNoCorsEnvironment ( ) : Promise < boolean > {
124
129
const uri = await vscode . env . asExternalUri ( vscode . Uri . parse ( `${ vscode . env . uriScheme } ://vscode.github-authentication/dummy` ) ) ;
@@ -130,9 +135,12 @@ export class GitHubServer implements IGitHubServer {
130
135
131
136
const callbackUri = await vscode . env . asExternalUri ( vscode . Uri . parse ( `${ vscode . env . uriScheme } ://vscode.github-authentication/did-authenticate` ) ) ;
132
137
133
- if ( this . isTestEnvironment ( callbackUri ) ) {
134
- const token = await vscode . window . showInputBox ( { prompt : 'GitHub Personal Access Token' , ignoreFocusOut : true } ) ;
135
- if ( ! token ) { throw new Error ( 'Sign in failed: No token provided' ) ; }
138
+ if ( ! isSupportedEnvironment ( callbackUri ) ) {
139
+ const token = this . _supportDeviceCodeFlow
140
+ ? await this . doDeviceCodeFlow ( scopes )
141
+ : await vscode . window . showInputBox ( { prompt : 'GitHub Personal Access Token' , ignoreFocusOut : true } ) ;
142
+
143
+ if ( ! token ) { throw new Error ( 'No token provided' ) ; }
136
144
137
145
const tokenScopes = await getScopes ( token , this . getServerUri ( '/' ) , this . _logger ) ; // Example: ['repo', 'user']
138
146
const scopesList = scopes . split ( ' ' ) ; // Example: 'read:user repo user:email'
@@ -187,6 +195,96 @@ export class GitHubServer implements IGitHubServer {
187
195
} ) ;
188
196
}
189
197
198
+ private async doDeviceCodeFlow ( scopes : string ) : Promise < string > {
199
+ // Get initial device code
200
+ const uri = `https://github.com/login/device/code?client_id=${ CLIENT_ID } &scope=${ scopes } ` ;
201
+ const result = await fetch ( uri , {
202
+ method : 'POST' ,
203
+ headers : {
204
+ Accept : 'application/json'
205
+ }
206
+ } ) ;
207
+ if ( ! result . ok ) {
208
+ throw new Error ( `Failed to get one-time code: ${ await result . text ( ) } ` ) ;
209
+ }
210
+
211
+ const json = await result . json ( ) as IGitHubDeviceCodeResponse ;
212
+
213
+ await vscode . env . clipboard . writeText ( json . user_code ) ;
214
+
215
+ const modalResult = await vscode . window . showInformationMessage (
216
+ localize ( 'code.title' , "Your Code: {0}" , json . user_code ) ,
217
+ {
218
+ modal : true ,
219
+ detail : localize ( 'code.detail' , "The above one-time code has been copied to your clipboard. To finish authenticating, paste it on GitHub." )
220
+ } , 'Continue to GitHub' ) ;
221
+
222
+ if ( modalResult !== 'Continue to GitHub' ) {
223
+ throw new Error ( 'Cancelled' ) ;
224
+ }
225
+
226
+ const uriToOpen = await vscode . env . asExternalUri ( vscode . Uri . parse ( json . verification_uri ) ) ;
227
+ await vscode . env . openExternal ( uriToOpen ) ;
228
+
229
+ return await vscode . window . withProgress < string > ( {
230
+ location : vscode . ProgressLocation . Notification ,
231
+ cancellable : true ,
232
+ title : localize (
233
+ 'progress' ,
234
+ "Open [{0}]({0}) in a new tab and paste your one-time code: {1}" ,
235
+ json . verification_uri ,
236
+ json . user_code )
237
+ } , async ( _ , token ) => {
238
+ return await this . waitForDeviceCodeAccessToken ( json , token ) ;
239
+ } ) ;
240
+ }
241
+
242
+ private async waitForDeviceCodeAccessToken (
243
+ json : IGitHubDeviceCodeResponse ,
244
+ token : vscode . CancellationToken
245
+ ) : Promise < string > {
246
+
247
+ const refreshTokenUri = `https://github.com/login/oauth/access_token?client_id=${ CLIENT_ID } &device_code=${ json . device_code } &grant_type=urn:ietf:params:oauth:grant-type:device_code` ;
248
+
249
+ // Try for 2 minutes
250
+ const attempts = 120 / json . interval ;
251
+ for ( let i = 0 ; i < attempts ; i ++ ) {
252
+ await new Promise ( resolve => setTimeout ( resolve , json . interval * 1000 ) ) ;
253
+ if ( token . isCancellationRequested ) {
254
+ throw new Error ( 'Cancelled' ) ;
255
+ }
256
+ let accessTokenResult ;
257
+ try {
258
+ accessTokenResult = await fetch ( refreshTokenUri , {
259
+ method : 'POST' ,
260
+ headers : {
261
+ Accept : 'application/json'
262
+ }
263
+ } ) ;
264
+ } catch {
265
+ continue ;
266
+ }
267
+
268
+ if ( ! accessTokenResult . ok ) {
269
+ continue ;
270
+ }
271
+
272
+ const accessTokenJson = await accessTokenResult . json ( ) ;
273
+
274
+ if ( accessTokenJson . error === 'authorization_pending' ) {
275
+ continue ;
276
+ }
277
+
278
+ if ( accessTokenJson . error ) {
279
+ throw new Error ( accessTokenJson . error_description ) ;
280
+ }
281
+
282
+ return accessTokenJson . access_token ;
283
+ }
284
+
285
+ throw new Error ( 'Cancelled' ) ;
286
+ }
287
+
190
288
private exchangeCodeForToken : ( scopes : string ) => PromiseAdapter < vscode . Uri , string > =
191
289
( scopes ) => async ( uri , resolve , reject ) => {
192
290
const query = parseQuery ( uri ) ;
0 commit comments