import { PrimaryClientConfiguration } from '../protocol/client_config' ; import { sendMessage } from '../protocol/comms' ; import { AUTHENTICATION_METHODS , OpenYoloCredential , OpenYoloCredentialHintOptions , OpenYoloCredentialRequestOptions } from '../protocol/data' ; import { OpenYoloInternalError } from '../protocol/errors' ; import { isOpenYoloMessageFormat } from '../protocol/messages' ; import { channelErrorMessage } from '../protocol/post_messages' ; import * as msg from '../protocol/rpc_messages' ; import { SecureChannel } from '../protocol/secure_channel' ; import { CancellablePromise } from '../protocol/utils' ; import { AncestorOriginVerifier } from './ancestor_origin_verifier' ; import { AffiliationProvider , CredentialDataProvider , DisplayCallbacks , InteractionProvider , LocalStateProvider , ProviderConfiguration , WindowLike } from './provider_config' ; /**
 * Handles request from the client.
 *
 * TODO: break down this class. It is untestable. Each request handler should
 * live in a separate class/function to abstract the messaging layer and to
 * allow testing more thoroughly the different paths.
 */ export class ProviderFrame { private clientAuthDomain : string ; private affiliationProvider : AffiliationProvider ; private localStateProvider : LocalStateProvider ; private credentialDataProvider : CredentialDataProvider ; private interactionProvider : InteractionProvider ; private requestInProgress = false ; private cancellable : CancellablePromise < never > | null = null ; private closeListener : EventListener ; private window : WindowLike ; private proxyLoginCredential : OpenYoloCredential | null = null ; /**
     * Performs the initial validation of the execution context, and then
     * instantiates a  ProviderFrame instance.
      static async initialize ( providerConfig ) { try { let clientConfiguration = await providerConfig . clientConfigurationProvider . getConfiguration ( providerConfig . clientAuthDomain ) ; if ( ! clientConfiguration || ! clientConfiguration . apiEnabled ) { throw OpenYoloInternalError . apiDisabled ( ) ; } let equivalentAuthDomains = await providerConfig . affiliationProvider . getEquivalentDomains ( providerConfig . clientAuthDomain ) ; let webAuthDomains = equivalentAuthDomains . filter ( ( origin ) => { return origin . startsWith ( 'http' ) ; } ) ; if ( clientConfiguration . allowNestedFrameRequests ) { await AncestorOriginVerifier . verify ( providerConfig . window , webAuthDomains ) ; } else { await AncestorOriginVerifier . verifyOnlyParent ( providerConfig . window , webAuthDomains ) ; } let secureChannel = await SecureChannel . providerConnect ( providerConfig . window , [ providerConfig . clientAuthDomain ] , providerConfig . clientNonce ) ; return new ProviderFrame ( providerConfig , secureChannel , clientConfiguration , equivalentAuthDomains ) ; } catch ( err ) { if ( err instanceof OpenYoloInternalError ) { sendMessage ( providerConfig . window . parent , channelErrorMessage ( err . toExposedError ( ) ) ) ; } else { sendMessage ( providerConfig . window . parent , channelErrorMessage ( OpenYoloInternalError . providerInitializationFailed ( ) . toExposedError ( ) ) ) ; } throw err ; } } constructor ( private providerConfig , private clientChannel , private clientConfig , private equivalentAuthDomains ) { this . clientAuthDomain = providerConfig . clientAuthDomain ; this . affiliationProvider = providerConfig . affiliationProvider ; this . credentialDataProvider = providerConfig . credentialDataProvider ; this . interactionProvider = providerConfig . interactionProvider ; this . localStateProvider = providerConfig . localStateProvider ; this . window = window ; this . registerListeners ( ) ; if ( this . window . opener ) { this . closeListener = ( ( ) => this . handleClose ( ) ) ; this . window . addEventListener ( 'beforeunload' , this . closeListener ) ; } } dispose ( ) { this . clientChannel . dispose ( ) ; this . interactionProvider . dispose ( ) ; this . window . removeEventListener ( 'beforeunload' , this . closeListener ) ; } private async handleClose ( ) { sendMessage ( this . providerConfig . window . parent , channelErrorMessage ( OpenYoloInternalError . userCanceled ( ) . toExposedError ( ) ) ) ; } private registerListeners ( ) { this . addRpcListener ( msg . RpcMessageType . disableAutoSignIn , ( m ) => this . handleDisableAutoSignInRequest ( m . id ) ) ; this . addRpcListener ( msg . RpcMessageType . retrieve , ( m ) => this . handleGetCredentialRequest ( m . id , m . args ) ) ; this . addRpcListener ( msg . RpcMessageType . hint , ( m ) => this . handleHintRequest ( m . id , m . args ) ) ; this . addRpcListener ( msg . RpcMessageType . hintAvailable , ( m ) => this . handleHintAvailableRequest ( m . id , m . args ) ) ; this . addRpcListener ( msg . RpcMessageType . save , ( m ) => this . handleSaveCredentialRequest ( m . id , m . args ) ) ; this . addRpcListener ( msg . RpcMessageType . proxy , ( m ) => this . handleProxyLoginRequest ( m . id , m . args ) ) ; this . addRpcListener ( msg . RpcMessageType . cancelLastOperation , ( m ) => this . handleCancelLastOperation ( m . id ) ) ; this . clientChannel . addFallbackListener ( ( ev ) => { this . handleUnknownMessage ( ev ) ; return false ; } ) ; } private addRpcListener < T extends msg . RpcMessageType > ( type , messageHandler ) { this . clientChannel . listen < T > ( type , ( m , type , ev ) => this . monitoringListener ( type , m , messageHandler ) ) ; } private async monitoringListener < T extends msg . RpcMessageType > ( type , m , messageHandler ) { if ( ! this . recordRequestStart ( type , m . id ) ) { return ; } try { await messageHandler ( m ) ; } catch ( error ) { if ( error && error === CancellablePromise . CANCELLED_ERROR ) { this . cancellable = null ; this . clientChannel . send ( msg . errorMessage ( m . id , OpenYoloInternalError . operationCanceled ( ) . toExposedError ( ) ) ) ; } else { throw error ; } } finally { this . recordRequestStop ( ) ; } } private recordRequestStart < T extends msg . RpcMessageType > ( requestType , requestId ) { if ( requestType === 'cancelLastOperation' ) { return true ; } if ( ! this . requestInProgress ) { this . requestInProgress = true ; return true ; } this . clientChannel . send ( msg . errorMessage ( requestId , OpenYoloInternalError . illegalConcurrentRequestError ( ) . toExposedError ( ) ) ) ; return false ; } private recordRequestStop ( ) { this . requestInProgress = false ; } private async handleDisableAutoSignInRequest ( requestId ) { try { await this . localStateProvider . setAutoSignInEnabled ( this . providerConfig . clientAuthDomain , false ) ; } catch ( e ) { console . error ( 'Failed to disable auto sign in.' ) ; } this . clientChannel . send ( msg . disableAutoSignInResultMessage ( requestId ) ) ; } private async handleHintRequest ( requestId , options ) { try { let hints = await this . cancellablePromise ( this . getHints ( options ) ) ; if ( hints . length < 1 ) { this . clientChannel . send ( msg . errorMessage ( requestId , OpenYoloInternalError . noCredentialsAvailable ( ) . toExposedError ( ) ) ) ; return ; } let selectionPromise = this . interactionProvider . showHintPicker ( hints , options , this . createDisplayCallbacks ( requestId ) ) ; let selectedHint = await this . cancellablePromise ( selectionPromise ) ; await this . cancellablePromise ( this . localStateProvider . setAutoSignInEnabled ( this . providerConfig . clientAuthDomain , true ) ) ; selectedHint = this . copyCredential ( selectedHint , true ) ; selectedHint . authDomain = this . clientAuthDomain ; this . clientChannel . send ( msg . credentialResultMessage ( requestId , selectedHint ) ) ; } catch ( err ) { this . handleWellKnownErrors ( err ) ; if ( err instanceof OpenYoloInternalError ) { this . clientChannel . send ( msg . errorMessage ( requestId , err . toExposedError ( ) ) ) ; } else { this . clientChannel . send ( msg . errorMessage ( requestId , OpenYoloInternalError . requestFailed ( 'Implementation error.' ) . toExposedError ( ) ) ) ; } } } private async handleHintAvailableRequest ( id , options ) { try { const hints = await this . cancellablePromise ( this . getHints ( options ) ) ; this . clientChannel . send ( msg . hintAvailableResponseMessage ( id , hints . length > 0 ) ) ; } catch ( err ) { this . handleWellKnownErrors ( err ) ; if ( err instanceof OpenYoloInternalError ) { this . clientChannel . send ( msg . errorMessage ( id , err . toExposedError ( ) ) ) ; } else { this . clientChannel . send ( msg . errorMessage ( id , OpenYoloInternalError . requestFailed ( 'Implementation error.' ) . toExposedError ( ) ) ) ; } } } private async handleGetCredentialRequest ( requestId , options ) { try { let credentials = await this . cancellablePromise ( this . credentialDataProvider . getAllCredentials ( this . equivalentAuthDomains , options ) ) ; let pertinentCredentials = credentials . filter ( ( credential ) => { return options . supportedAuthMethods . find ( ( value ) => value === credential . authMethod ) ; } ) ; if ( pertinentCredentials . length < 1 ) { this . clientChannel . send ( msg . errorMessage ( requestId , OpenYoloInternalError . noCredentialsAvailable ( ) . toExposedError ( ) ) ) ; return ; } if ( pertinentCredentials . length === 1 ) { let autoSignInEnabled = await this . localStateProvider . isAutoSignInEnabled ( this . clientAuthDomain ) ; if ( autoSignInEnabled ) { const credential = pertinentCredentials [ 0 ] ; await this . cancellablePromise ( this . interactionProvider . showAutoSignIn ( credential , this . createDisplayCallbacks ( requestId ) , options ) ) ; this . clientChannel . send ( msg . credentialResultMessage ( requestId , this . storeForProxyLogin ( credential ) ) ) ; return ; } } let selectionPromise = this . interactionProvider . showCredentialPicker ( pertinentCredentials , options , this . createDisplayCallbacks ( requestId ) ) ; let selectedCredential = await this . cancellablePromise ( selectionPromise ) ; await this . cancellablePromise ( this . localStateProvider . setAutoSignInEnabled ( this . providerConfig . clientAuthDomain , true ) ) ; this . clientChannel . send ( msg . credentialResultMessage ( requestId , this . storeForProxyLogin ( selectedCredential ) ) ) ; } catch ( err ) { this . handleWellKnownErrors ( err ) ; if ( err instanceof OpenYoloInternalError ) { this . clientChannel . send ( msg . errorMessage ( requestId , err . toExposedError ( ) ) ) ; } else { this . clientChannel . send ( msg . errorMessage ( requestId , OpenYoloInternalError . requestFailed ( 'Implementation error.' ) . toExposedError ( ) ) ) ; } } } private async handleSaveCredentialRequest ( id , credential ) { return this . handleUnimplementedRequest ( id , msg . RpcMessageType . save ) ; } private async handleProxyLoginRequest ( id , credential ) { return this . handleUnimplementedRequest ( id , msg . RpcMessageType . proxy ) ; } private async handleCancelLastOperation ( id ) { if ( ! this . requestInProgress || this . cancellable === null ) { } else { try { this . cancellable . cancel ( ) ; } finally { this . interactionProvider . dispose ( ) ; } } this . clientChannel . send ( msg . cancelLastOperationResultMessage ( id ) ) ; } /**
     * Responds to unknown request types with an error message.
     */ private async handleUnimplementedRequest ( id , type ) { this . clientChannel . send ( msg . errorMessage ( id , OpenYoloInternalError . unknownRequest ( type ) . toExposedError ( ) ) ) ; } private handleUnknownMessage ( ev ) { if ( ! isOpenYoloMessageFormat ( ev . data ) ) { return ; } if ( msg . RPC_MESSAGE_TYPES . indexOf ( ev . data . type ) === - 1 ) { return ; } const type = ( ev . data . type as msg . RpcMessageType ) ; if ( ! msg . RPC_MESSAGE_DATA_VALIDATORS [ type ] ( ev . data . data ) ) { return ; } const data = ( ev . data . data as msg . RpcMessageData < any > ) ; this . clientChannel . send ( msg . errorMessage ( data . id , OpenYoloInternalError . unknownRequest ( type ) . toExposedError ( ) ) ) ; return ; } /**
     * If direct auth is not permitted for this credential, store it in
     * {@code this.proxyLoginCredential} and return a redacted version of the
     * credential. Otherwise, just an unredacted copy of the credential.
     */ private storeForProxyLogin ( credential ) { if ( ! credential . password || ( this . providerConfig . allowDirectAuth && ! this . clientConfig . requireProxyLogin ) ) { return this . copyCredential ( credential , false ) ; } this . proxyLoginCredential = credential ; let redactedCopy = this . copyCredential ( credential , true ) ; redactedCopy . proxiedAuthRequired = true ; return redactedCopy ; } /**
     * Provides a shallow copy of a credential, optionally redacting sensitive
     * data.
     */ private copyCredential ( credential , redactSensitive ? ) { let redact = ! ! redactSensitive ; let copy = { id : credential . id , authMethod : credential . authMethod } ; if ( credential . authDomain ) { copy . authDomain = credential . authDomain ; } if ( credential . displayName ) { copy . displayName = credential . displayName ; } if ( credential . profilePicture ) { copy . profilePicture = credential . profilePicture ; } if ( credential . exchangeToken ) { copy . exchangeToken = credential . exchangeToken ; } if ( credential . idToken ) { copy . idToken = credential . idToken ; } if ( credential . newGrant ) { copy . newGrant = credential . newGrant ; } if ( ! redact && 'password' in credential ) { copy . password = credential . password ; } return copy ; } private createDisplayCallbacks ( requestId ) { return { requestDisplayOptions : ( options ) : Promise < void > => { return this . clientChannel . sendAndWaitAck ( msg . showProviderMessage ( requestId , options ) ) ; } } ; } /**
     * Creates a list of all hints that are compatible with the specified hint
     * options, ordered from most- to least- frequently used.
     */ private async getHints ( options ) { let allCredentials = await this . cancellablePromise ( this . credentialDataProvider . getAllHints ( options ) ) ; if ( allCredentials . length < 1 ) { return [ ] ; } let credentialsById = ( { } as { [ key ] : OpenYoloCredential ; } ) ; let credentialCount = ( { } as { [ key ] : number ; } ) ; let numRetained = 0 ; allCredentials . forEach ( ( credential ) => { if ( options . supportedAuthMethods . indexOf ( credential . authMethod ) < 0 ) { return ; } if ( ! ( credential . id in credentialsById ) ) { credentialsById [ credential . id ] = credential ; credentialCount [ credential . id ] = 1 ; numRetained ++ ; return ; } if ( this . completenessScore ( credential ) > this . completenessScore ( credentialsById [ credential . id ] ) ) { credentialsById [ credential . id ] = credential ; } credentialCount [ credential . id ] += 1 ; } ) ; if ( numRetained < 1 ) { return [ ] ; } let hintCredentials = [ ] ; for ( let credentialId in credentialsById ) { if ( credentialsById . hasOwnProperty ( credentialId ) ) { hintCredentials . push ( credentialsById [ credentialId ] ) ; } } return hintCredentials . sort ( ( a , b ) => { let aCount = credentialCount [ a . id ] ; let bCount = credentialCount [ b . id ] ; if ( aCount < bCount ) { return 1 ; } else if ( aCount > bCount ) { return - 1 ; } else { return 0 ; } } ) ; } /**
     * Calculates a "score" for a credential based on how complete the set of
     * information it contains is. This can be used to compare credentials and
     * favor those with more information.
     */ private completenessScore ( credential ) { let score = 0 ; if ( credential . authMethod !== AUTHENTICATION_METHODS . ID_AND_PASSWORD ) { score += 4 ; } if ( credential . displayName ) { score += 2 ; } if ( credential . profilePicture ) { score += 1 ; } return score ; } private cancellablePromise < T > ( producer ) { if ( ! this . cancellable ) { this . cancellable = new CancellablePromise ( ) ; } return Promise . race ( [ this . cancellable . promise , producer ] ) as Promise < T > ; } private handleWellKnownErrors ( error ) { if ( error === CancellablePromise . CANCELLED_ERROR ) { throw error ; } } }