@@ -17,7 +17,7 @@ import { Commands } from '../shared/vscode/commands2'
17
17
import { createQuickPick , DataQuickPickItem , showQuickPick } from '../shared/ui/pickerPrompter'
18
18
import { isValidResponse } from '../shared/wizards/wizard'
19
19
import { CancellationError } from '../shared/utilities/timeoutUtils'
20
- import { ToolkitError , UnknownError } from '../shared/errors'
20
+ import { formatError , ToolkitError , UnknownError } from '../shared/errors'
21
21
import { getCache } from './sso/cache'
22
22
import { createFactoryFunction , Mutable } from '../shared/utilities/tsUtils'
23
23
import { builderIdStartUrl , SsoToken } from './sso/model'
@@ -38,6 +38,7 @@ import { getCodeCatalystDevEnvId } from '../shared/vscode/env'
38
38
import { getConfigFilename } from './sharedCredentials'
39
39
import { authHelpUrl } from '../shared/constants'
40
40
import { getDependentAuths } from './secondaryAuth'
41
+ import { DevSettings } from '../shared/settings'
41
42
42
43
export const ssoScope = 'sso:account:access'
43
44
export const codecatalystScopes = [ 'codecatalyst:read_write' ]
@@ -301,7 +302,8 @@ interface ConnectionStateChangeEvent {
301
302
}
302
303
303
304
export class Auth implements AuthService , ConnectionManager {
304
- private readonly ssoCache = getCache ( )
305
+ readonly #ssoCache = getCache ( )
306
+ readonly #validationErrors = new Map < Connection [ 'id' ] , Error > ( )
305
307
readonly #onDidChangeActiveConnection = new vscode . EventEmitter < StatefulConnection | undefined > ( )
306
308
readonly #onDidChangeConnectionState = new vscode . EventEmitter < ConnectionStateChangeEvent > ( )
307
309
public readonly onDidChangeActiveConnection = this . #onDidChangeActiveConnection. event
@@ -455,6 +457,10 @@ export class Auth implements AuthService, ConnectionManager {
455
457
return this . store . getProfile ( connection . id ) ?. metadata . connectionState
456
458
}
457
459
460
+ public getInvalidationReason ( connection : Pick < Connection , 'id' > ) : Error | undefined {
461
+ return this . #validationErrors. get ( connection . id )
462
+ }
463
+
458
464
/**
459
465
* Attempts to remove all auth state related to the connection.
460
466
*
@@ -487,6 +493,10 @@ export class Auth implements AuthService, ConnectionManager {
487
493
}
488
494
489
495
const profile = await this . store . updateProfile ( id , { connectionState } )
496
+ if ( connectionState !== 'invalid' ) {
497
+ this . #validationErrors. delete ( id )
498
+ }
499
+
490
500
if ( this . #activeConnection?. id === id ) {
491
501
this . #activeConnection. state = connectionState
492
502
this . #onDidChangeActiveConnection. fire ( this . #activeConnection)
@@ -497,16 +507,16 @@ export class Auth implements AuthService, ConnectionManager {
497
507
}
498
508
499
509
private async validateConnection < T extends Profile > ( id : Connection [ 'id' ] , profile : StoredProfile < T > ) {
500
- if ( profile . type === 'sso' ) {
501
- const provider = this . getTokenProvider ( id , profile )
502
- if ( ( await provider . getToken ( ) ) === undefined ) {
503
- return this . updateConnectionState ( id , 'invalid' )
510
+ const runCheck = async ( ) => {
511
+ if ( profile . type === 'sso' ) {
512
+ const provider = this . getTokenProvider ( id , profile )
513
+ if ( ( await provider . getToken ( ) ) === undefined ) {
514
+ return this . updateConnectionState ( id , 'invalid' )
515
+ } else {
516
+ return this . updateConnectionState ( id , 'valid' )
517
+ }
504
518
} else {
505
- return this . updateConnectionState ( id , 'valid' )
506
- }
507
- } else {
508
- const provider = await this . getCredentialsProvider ( id )
509
- try {
519
+ const provider = await this . getCredentialsProvider ( id )
510
520
const credentials = await this . getCachedCredentials ( provider )
511
521
if ( credentials !== undefined ) {
512
522
return this . updateConnectionState ( id , 'valid' )
@@ -517,10 +527,16 @@ export class Auth implements AuthService, ConnectionManager {
517
527
} else {
518
528
return this . updateConnectionState ( id , 'invalid' )
519
529
}
520
- } catch {
521
- return this . updateConnectionState ( id , 'invalid' )
522
530
}
523
531
}
532
+
533
+ return runCheck ( ) . catch ( err => this . handleValidationError ( id , err ) )
534
+ }
535
+
536
+ private async handleValidationError ( id : Connection [ 'id' ] , err : unknown ) {
537
+ this . #validationErrors. set ( id , UnknownError . cast ( err ) )
538
+
539
+ return this . updateConnectionState ( id , 'invalid' )
524
540
}
525
541
526
542
private async getCredentialsProvider ( id : Connection [ 'id' ] ) {
@@ -564,7 +580,7 @@ export class Auth implements AuthService, ConnectionManager {
564
580
scopes : profile . scopes ,
565
581
region : profile . ssoRegion ,
566
582
} ,
567
- this . ssoCache
583
+ this . # ssoCache
568
584
)
569
585
}
570
586
@@ -612,7 +628,7 @@ export class Auth implements AuthService, ConnectionManager {
612
628
613
629
return result
614
630
} catch ( err ) {
615
- await this . updateConnectionState ( id , 'invalid' )
631
+ await this . handleValidationError ( id , err )
616
632
throw err
617
633
}
618
634
}
@@ -635,7 +651,9 @@ export class Auth implements AuthService, ConnectionManager {
635
651
636
652
private readonly getToken = keyedDebounce ( this . _getToken . bind ( this ) )
637
653
private async _getToken ( id : Connection [ 'id' ] , provider : SsoAccessTokenProvider ) : Promise < SsoToken > {
638
- const token = await provider . getToken ( )
654
+ const token = await provider . getToken ( ) . catch ( err => {
655
+ this . #validationErrors. set ( id , err )
656
+ } )
639
657
640
658
return token ?? this . handleInvalidCredentials ( id , ( ) => provider . createToken ( ) )
641
659
}
@@ -659,6 +677,7 @@ export class Auth implements AuthService, ConnectionManager {
659
677
if ( previousState === 'invalid' ) {
660
678
throw new ToolkitError ( 'Connection is invalid or expired. Try logging in again.' , {
661
679
code : 'InvalidConnection' ,
680
+ cause : this . #validationErrors. get ( id ) ,
662
681
} )
663
682
}
664
683
// TODO: cancellable notification?
@@ -669,6 +688,7 @@ export class Auth implements AuthService, ConnectionManager {
669
688
throw new ToolkitError ( 'User cancelled login' , {
670
689
cancelled : true ,
671
690
code : 'InvalidConnection' ,
691
+ cause : this . #validationErrors. get ( id ) ,
672
692
} )
673
693
}
674
694
}
@@ -1003,47 +1023,57 @@ export function createConnectionPrompter(auth: Auth, type?: 'iam' | 'sso') {
1003
1023
1004
1024
function toPickerItem ( conn : Connection ) : DataQuickPickItem < Connection > {
1005
1025
const state = auth . getConnectionState ( conn )
1006
- if ( state ! == 'valid' ) {
1026
+ if ( state = == 'valid' ) {
1007
1027
return {
1008
1028
data : conn ,
1009
- invalidSelection : true ,
1010
- label : codicon `${ getIcon ( 'vscode-error' ) } ${ conn . label } ` ,
1011
- description :
1012
- state === 'authenticating'
1013
- ? 'authenticating...'
1014
- : localize (
1015
- 'aws.auth.promptConnection.expired.description' ,
1016
- 'Expired or Invalid, select to authenticate'
1017
- ) ,
1018
- onClick :
1019
- state !== 'authenticating'
1020
- ? async ( ) => {
1021
- // XXX: this is hack because only 1 picker can be shown at a time
1022
- // Some legacy auth providers will show a picker, hiding this one
1023
- // If we detect this then we'll jump straight into using the connection
1024
- let hidden = false
1025
- const sub = prompter . quickPick . onDidHide ( ( ) => {
1026
- hidden = true
1027
- sub . dispose ( )
1028
- } )
1029
- const newConn = await reauthCommand . execute ( auth , conn )
1030
- if ( hidden && newConn && auth . getConnectionState ( newConn ) === 'valid' ) {
1031
- await auth . useConnection ( newConn )
1032
- } else {
1033
- await prompter . clearAndLoadItems ( loadItems ( ) )
1034
- prompter . selectItems (
1035
- ...prompter . quickPick . items . filter ( i => i . label . includes ( conn . label ) )
1036
- )
1037
- }
1038
- }
1039
- : undefined ,
1029
+ label : codicon `${ getConnectionIcon ( conn ) } ${ conn . label } ` ,
1030
+ description : getConnectionDescription ( conn ) ,
1031
+ }
1032
+ }
1033
+
1034
+ const getDetail = ( ) => {
1035
+ if ( ! DevSettings . instance . get ( 'renderDebugDetails' , false ) ) {
1036
+ return
1040
1037
}
1038
+
1039
+ const err = auth . getInvalidationReason ( conn )
1040
+ return err ? formatError ( err ) : undefined
1041
1041
}
1042
1042
1043
1043
return {
1044
+ detail : getDetail ( ) ,
1044
1045
data : conn ,
1045
- label : codicon `${ getConnectionIcon ( conn ) } ${ conn . label } ` ,
1046
- description : getConnectionDescription ( conn ) ,
1046
+ invalidSelection : true ,
1047
+ label : codicon `${ getIcon ( 'vscode-error' ) } ${ conn . label } ` ,
1048
+ description :
1049
+ state === 'authenticating'
1050
+ ? 'authenticating...'
1051
+ : localize (
1052
+ 'aws.auth.promptConnection.expired.description' ,
1053
+ 'Expired or Invalid, select to authenticate'
1054
+ ) ,
1055
+ onClick :
1056
+ state !== 'authenticating'
1057
+ ? async ( ) => {
1058
+ // XXX: this is hack because only 1 picker can be shown at a time
1059
+ // Some legacy auth providers will show a picker, hiding this one
1060
+ // If we detect this then we'll jump straight into using the connection
1061
+ let hidden = false
1062
+ const sub = prompter . quickPick . onDidHide ( ( ) => {
1063
+ hidden = true
1064
+ sub . dispose ( )
1065
+ } )
1066
+ const newConn = await reauthCommand . execute ( auth , conn )
1067
+ if ( hidden && newConn && auth . getConnectionState ( newConn ) === 'valid' ) {
1068
+ await auth . useConnection ( newConn )
1069
+ } else {
1070
+ await prompter . clearAndLoadItems ( loadItems ( ) )
1071
+ prompter . selectItems (
1072
+ ...prompter . quickPick . items . filter ( i => i . label . includes ( conn . label ) )
1073
+ )
1074
+ }
1075
+ }
1076
+ : undefined ,
1047
1077
}
1048
1078
}
1049
1079
0 commit comments