@@ -70,16 +70,19 @@ export const LoginTypes = {
7070} as const
7171export type LoginType = ( typeof LoginTypes ) [ keyof typeof LoginTypes ]
7272
73- interface BaseLogin {
74- readonly loginType : LoginType
75- }
76-
7773export type cacheChangedEvent = 'delete' | 'create'
7874
79- export type Login = SsoLogin // TODO: add IamLogin type when supported
75+ export type Login = SsoLogin | IamLogin
8076
8177export type TokenSource = IamIdentityCenterSsoTokenSource | AwsBuilderIdSsoTokenSource
8278
79+ /**
80+ * Interface for authentication management
81+ */
82+ interface BaseLogin {
83+ readonly loginType : LoginType
84+ }
85+
8386/**
8487 * Handles auth requests to the Identity Server in the Amazon Q LSP.
8588 */
@@ -188,7 +191,6 @@ export class LanguageClientAuth {
188191 */
189192export class SsoLogin implements BaseLogin {
190193 readonly loginType = LoginTypes . SSO
191- private readonly eventEmitter = new vscode . EventEmitter < AuthStateEvent > ( )
192194
193195 // Cached information from the identity server for easy reference
194196 private ssoTokenId : string | undefined
@@ -199,7 +201,8 @@ export class SsoLogin implements BaseLogin {
199201
200202 constructor (
201203 public readonly profileName : string ,
202- private readonly lspAuth : LanguageClientAuth
204+ private readonly lspAuth : LanguageClientAuth ,
205+ private readonly eventEmitter : vscode . EventEmitter < AuthStateEvent >
203206 ) {
204207 lspAuth . registerSsoTokenChangedHandler ( ( params : SsoTokenChangedParams ) => this . ssoTokenChangedHandler ( params ) )
205208 }
@@ -341,8 +344,184 @@ export class SsoLogin implements BaseLogin {
341344 return this . connectionState
342345 }
343346
344- onDidChangeConnectionState ( handler : ( e : AuthStateEvent ) => any ) {
345- return this . eventEmitter . event ( handler )
347+ private updateConnectionState ( state : AuthState ) {
348+ const oldState = this . connectionState
349+ const newState = state
350+
351+ this . connectionState = newState
352+
353+ if ( oldState !== newState ) {
354+ this . eventEmitter . fire ( { id : this . profileName , state : this . connectionState } )
355+ }
356+ }
357+
358+ private ssoTokenChangedHandler ( params : SsoTokenChangedParams ) {
359+ if ( params . ssoTokenId === this . ssoTokenId ) {
360+ if ( params . kind === SsoTokenChangedKind . Expired ) {
361+ this . updateConnectionState ( 'expired' )
362+ return
363+ } else if ( params . kind === SsoTokenChangedKind . Refreshed ) {
364+ this . eventEmitter . fire ( { id : this . profileName , state : 'refreshed' } )
365+ }
366+ }
367+ }
368+ }
369+
370+ /**
371+ * Manages an IAM credentials connection.
372+ */
373+ export class IamLogin implements BaseLogin {
374+ readonly loginType = LoginTypes . IAM
375+
376+ // Cached information from the identity server for easy reference
377+ private ssoTokenId : string | undefined
378+ private connectionState : AuthState = 'notConnected'
379+ private _data : { startUrl : string ; region : string } | undefined
380+
381+ private cancellationToken : CancellationTokenSource | undefined
382+
383+ constructor (
384+ public readonly profileName : string ,
385+ private readonly lspAuth : LanguageClientAuth ,
386+ private readonly eventEmitter : vscode . EventEmitter < AuthStateEvent >
387+ ) {
388+ lspAuth . registerSsoTokenChangedHandler ( ( params : SsoTokenChangedParams ) => this . ssoTokenChangedHandler ( params ) )
389+ }
390+
391+ get data ( ) {
392+ return this . _data
393+ }
394+
395+ async login ( opts : { accessKey : string ; secretKey : string } ) {
396+ // await this.updateProfile(opts)
397+ return this . _getSsoToken ( true )
398+ }
399+
400+ async reauthenticate ( ) {
401+ if ( this . connectionState === 'notConnected' ) {
402+ throw new ToolkitError ( 'Cannot reauthenticate when not connected.' )
403+ }
404+ return this . _getSsoToken ( true )
405+ }
406+
407+ async logout ( ) {
408+ if ( this . ssoTokenId ) {
409+ await this . lspAuth . invalidateSsoToken ( this . ssoTokenId )
410+ }
411+ this . updateConnectionState ( 'notConnected' )
412+ this . _data = undefined
413+ // TODO: DeleteProfile api in Identity Service (this doesn't exist yet)
414+ }
415+
416+ async getProfile ( ) {
417+ return await this . lspAuth . getProfile ( this . profileName )
418+ }
419+
420+ async updateProfile ( opts : { startUrl : string ; region : string ; scopes : string [ ] } ) {
421+ await this . lspAuth . updateProfile ( this . profileName , opts . startUrl , opts . region , opts . scopes )
422+ this . _data = {
423+ startUrl : opts . startUrl ,
424+ region : opts . region ,
425+ }
426+ }
427+
428+ /**
429+ * Restore the connection state and connection details to memory, if they exist.
430+ */
431+ async restore ( ) {
432+ // const sessionData = await this.getProfile()
433+ // const ssoSession = sessionData?.ssoSession?.settings
434+ // if (ssoSession?.sso_region && ssoSession?.sso_start_url) {
435+ // this._data = {
436+ // startUrl: ssoSession.sso_start_url,
437+ // region: ssoSession.sso_region,
438+ // }
439+ // }
440+ // try {
441+ // await this._getSsoToken(false)
442+ // } catch (err) {
443+ // getLogger().error('Restoring connection failed: %s', err)
444+ // }
445+ }
446+
447+ /**
448+ * Cancels running active login flows.
449+ */
450+ cancelLogin ( ) {
451+ this . cancellationToken ?. cancel ( )
452+ this . cancellationToken ?. dispose ( )
453+ this . cancellationToken = undefined
454+ }
455+
456+ /**
457+ * Returns both the decrypted access token and the payload to send to the `updateCredentials` LSP API
458+ * with encrypted token
459+ */
460+ async getToken ( ) {
461+ const response = await this . _getSsoToken ( false )
462+ const decryptedKey = await jose . compactDecrypt ( response . ssoToken . accessToken , this . lspAuth . encryptionKey )
463+ return {
464+ token : decryptedKey . plaintext . toString ( ) . replaceAll ( '"' , '' ) ,
465+ updateCredentialsParams : response . updateCredentialsParams ,
466+ }
467+ }
468+
469+ /**
470+ * Returns the response from `getSsoToken` LSP API and sets the connection state based on the errors/result
471+ * of the call.
472+ */
473+ private async _getSsoToken ( login : boolean ) {
474+ let response : GetSsoTokenResult
475+ this . cancellationToken = new CancellationTokenSource ( )
476+
477+ try {
478+ response = await this . lspAuth . getSsoToken (
479+ {
480+ /**
481+ * Note that we do not use SsoTokenSourceKind.AwsBuilderId here.
482+ * This is because it does not leave any state behind on disk, so
483+ * we cannot infer that a builder ID connection exists via the
484+ * Identity Server alone.
485+ */
486+ kind : SsoTokenSourceKind . IamIdentityCenter ,
487+ profileName : this . profileName ,
488+ } satisfies IamIdentityCenterSsoTokenSource ,
489+ login ,
490+ this . cancellationToken . token
491+ )
492+ } catch ( err : any ) {
493+ switch ( err . data ?. awsErrorCode ) {
494+ case AwsErrorCodes . E_CANCELLED :
495+ case AwsErrorCodes . E_SSO_SESSION_NOT_FOUND :
496+ case AwsErrorCodes . E_PROFILE_NOT_FOUND :
497+ case AwsErrorCodes . E_INVALID_SSO_TOKEN :
498+ this . updateConnectionState ( 'notConnected' )
499+ break
500+ case AwsErrorCodes . E_CANNOT_REFRESH_SSO_TOKEN :
501+ this . updateConnectionState ( 'expired' )
502+ break
503+ // TODO: implement when identity server emits E_NETWORK_ERROR, E_FILESYSTEM_ERROR
504+ // case AwsErrorCodes.E_NETWORK_ERROR:
505+ // case AwsErrorCodes.E_FILESYSTEM_ERROR:
506+ // // do stuff, probably nothing at all
507+ // break
508+ default :
509+ getLogger ( ) . error ( 'SsoLogin: unknown error when requesting token: %s' , err )
510+ break
511+ }
512+ throw err
513+ } finally {
514+ this . cancellationToken ?. dispose ( )
515+ this . cancellationToken = undefined
516+ }
517+
518+ this . ssoTokenId = response . ssoToken . id
519+ this . updateConnectionState ( 'connected' )
520+ return response
521+ }
522+
523+ getConnectionState ( ) {
524+ return this . connectionState
346525 }
347526
348527 private updateConnectionState ( state : AuthState ) {
0 commit comments