import { Buffer } from '@stacks/common' ; import { AppConfig } from './appConfig' ; import { SessionOptions } from './sessionData' ; import { InstanceDataStore , LocalStorageStore , SessionDataStore } from './sessionStore' ; import { decodeToken } from 'jsontokens' ; import { verifyAuthResponse } from './verification' ; import * as authMessages from './messages' ; import { decryptContent , encryptContent , EncryptContentOptions , hexStringToECPair , } from '@stacks/encryption' ; import { getAddressFromDID } from './dids' ; import { BLOCKSTACK_DEFAULT_GAIA_HUB_URL , fetchPrivate , getGlobalObject , InvalidStateError , isLaterVersion , Logger , LoginFailedError , MissingParameterError , nextHour , } from '@stacks/common' ; import { extractProfile } from '@stacks/profile' ; import { AuthScope , DEFAULT_PROFILE , NAME_LOOKUP_PATH } from './constants' ; import * as queryString from 'query-string' ; import { UserData } from './userData' ; import { StacksMainnet } from '@stacks/network' ; import { protocolEchoReplyDetection } from './protocolEchoDetection' ; /**
 *
 * Represents an instance of a signed in user for a particular app.
 *
 * A signed in user has access to two major pieces of information
 * about the user, the user's private key for that app and the location
 * of the user's gaia storage bucket for the app.
 *
 * A user can be signed in either directly through the interactive
 * sign in process or by directly providing the app private key.
 *

 *
 */ export class UserSession { appConfig : AppConfig ; store : SessionDataStore ; param options constructor ( options ? ) { let runningInBrowser = true ; if ( typeof window === 'undefined' && typeof self === 'undefined' ) { runningInBrowser = false ; } if ( options && options . appConfig ) { this . appConfig = options . appConfig ; } else if ( runningInBrowser ) { this . appConfig = new AppConfig ( ) ; } else { throw new MissingParameterError ( 'You need to specify options.appConfig' ) ; } if ( options && options . sessionStore ) { this . store = options . sessionStore ; } else if ( runningInBrowser ) { if ( options ) { this . store = new LocalStorageStore ( options . sessionOptions ) ; } else { this . store = new LocalStorageStore ( ) ; } } else if ( options ) { this . store = new InstanceDataStore ( options . sessionOptions ) ; } else { this . store = new InstanceDataStore ( ) ; } } param transitKey param redirectURI param manifestURI param scopes param appDomain param expiresAt param extraParams returns { String } makeAuthRequest ( transitKey ? , redirectURI ? , manifestURI ? , scopes ? , appDomain ? , expiresAt = nextHour ( ) . getTime ( ) , extraParams = { } ) { const appConfig = this . appConfig ; if ( ! appConfig ) { throw new InvalidStateError ( 'Missing AppConfig' ) ; } transitKey = transitKey || this . generateAndStoreTransitKey ( ) ; redirectURI = redirectURI || appConfig . redirectURI ( ) ; manifestURI = manifestURI || appConfig . manifestURI ( ) ; scopes = scopes || appConfig . scopes ; appDomain = appDomain || appConfig . appDomain ; return authMessages . makeAuthRequest ( transitKey , redirectURI , manifestURI , scopes , appDomain , expiresAt , extraParams ) ; } returns { String } generateAndStoreTransitKey ( ) { const sessionData = this . store . getSessionData ( ) ; const transitKey = authMessages . generateTransitKey ( ) ; sessionData . transitKey = transitKey ; this . store . setSessionData ( sessionData ) ; return transitKey ; } return { String } getAuthResponseToken ( ) { const search = getGlobalObject ( 'location' , { throwIfUnavailable : true , usageDesc : 'getAuthResponseToken' , } ) ?. search ; if ( search ) { const queryDict = queryString . parse ( search ) ; return queryDict . authResponse ? ( queryDict . authResponse as string ) : '' ; } return '' ; } return { Boolean } isSignInPending ( ) { try { const isProtocolEcho = protocolEchoReplyDetection ( ) ; if ( isProtocolEcho ) { Logger . info ( 'protocolEchoReply detected from isSignInPending call, the page is about to redirect.' ) ; return true ; } } catch ( error ) { Logger . error ( `Error checking for protocol echo reply isSignInPending: ${ error }` ) ; } return ! ! this . getAuthResponseToken ( ) ; } returns { Boolean } isUserSignedIn ( ) { return ! ! this . store . getSessionData ( ) . userData ; } param { String } authResponseToken returns { Promise } async handlePendingSignIn ( authResponseToken = this . getAuthResponseToken ( ) ) { const sessionData = this . store . getSessionData ( ) ; if ( sessionData . userData ) { throw new LoginFailedError ( 'Existing user session found.' ) ; } const transitKey = this . store . getSessionData ( ) . transitKey ; let coreNode = this . appConfig && this . appConfig . coreNode ; if ( ! coreNode ) { const network = new StacksMainnet ( ) ; coreNode = network . bnsLookupUrl ; } const tokenPayload = decodeToken ( authResponseToken ) . payload ; if ( typeof tokenPayload === 'string' ) { throw new Error ( 'Unexpected token payload type of string' ) ; } const nameLookupURL = `${ coreNode }${ NAME_LOOKUP_PATH }` ; const fallbackLookupURLs = [ `https://stacks-node-api.stacks.co${ NAME_LOOKUP_PATH }` , `https://registrar.stacks.co${ NAME_LOOKUP_PATH }` , ] . filter ( url => url !== nameLookupURL ) ; const isValid = await verifyAuthResponse ( authResponseToken , nameLookupURL , fallbackLookupURLs ) ; if ( ! isValid ) { throw new LoginFailedError ( 'Invalid authentication response.' ) ; } let appPrivateKey = tokenPayload . private_key as string ; let coreSessionToken = tokenPayload . core_token as string ; if ( isLaterVersion ( tokenPayload . version as string , '1.1.0' ) ) { if ( transitKey !== undefined && transitKey != null ) { if ( tokenPayload . private_key !== undefined && tokenPayload . private_key !== null ) { try { appPrivateKey = ( await authMessages . decryptPrivateKey ( transitKey , tokenPayload . private_key as string ) ) as string ; } catch ( e ) { Logger . warn ( 'Failed decryption of appPrivateKey, will try to use as given' ) ; try { hexStringToECPair ( tokenPayload . private_key as string ) ; } catch ( ecPairError ) { throw new LoginFailedError ( 'Failed decrypting appPrivateKey. Usually means' + ' that the transit key has changed during login.' ) ; } } } if ( coreSessionToken !== undefined && coreSessionToken !== null ) { try { coreSessionToken = ( await authMessages . decryptPrivateKey ( transitKey , coreSessionToken ) ) as string ; } catch ( e ) { Logger . info ( 'Failed decryption of coreSessionToken, will try to use as given' ) ; } } } else { throw new LoginFailedError ( 'Authenticating with protocol > 1.1.0 requires transit' + ' key, and none found.' ) ; } } let hubUrl = BLOCKSTACK_DEFAULT_GAIA_HUB_URL ; let gaiaAssociationToken ; if ( isLaterVersion ( tokenPayload . version as string , '1.2.0' ) && tokenPayload . hubUrl !== null && tokenPayload . hubUrl !== undefined ) { hubUrl = tokenPayload . hubUrl as string ; } if ( isLaterVersion ( tokenPayload . version as string , '1.3.0' ) && tokenPayload . associationToken !== null && tokenPayload . associationToken !== undefined ) { gaiaAssociationToken = tokenPayload . associationToken as string ; } const userData = { username : tokenPayload . username as string , profile : tokenPayload . profile , email : tokenPayload . email as string , decentralizedID : tokenPayload . iss , identityAddress : getAddressFromDID ( tokenPayload . iss ) , appPrivateKey , coreSessionToken , authResponseToken , hubUrl , coreNode : tokenPayload . blockstackAPIUrl as string , gaiaAssociationToken , } ; const profileURL = tokenPayload . profile_url as string ; if ( ! userData . profile && profileURL ) { const response = await fetchPrivate ( profileURL ) ; if ( ! response . ok ) { userData . profile = Object . assign ( { } , DEFAULT_PROFILE ) ; } else { const responseText = await response . text ( ) ; const wrappedProfile = JSON . parse ( responseText ) ; userData . profile = extractProfile ( wrappedProfile [ 0 ] . token ) ; } } else { userData . profile = tokenPayload . profile ; } sessionData . userData = userData ; this . store . setSessionData ( sessionData ) ; return userData ; } returns { Object } loadUserData ( ) { const userData = this . store . getSessionData ( ) . userData ; if ( ! userData ) { throw new InvalidStateError ( 'No user data found. Did the user sign in?' ) ; } return userData ; } param { String | Buffer } content param options param { String } options . publicKey returns { String } encryptContent ( content , options ? ) { const opts = Object . assign ( { } , options ) ; if ( ! opts . privateKey ) { opts . privateKey = this . loadUserData ( ) . appPrivateKey ; } return encryptContent ( content , opts ) ; } param { String | Buffer } content param options param { String } options . privateKey returns { String | Buffer } decryptContent ( content , options ? ) { const opts = Object . assign ( { } , options ) ; if ( ! opts . privateKey ) { opts . privateKey = this . loadUserData ( ) . appPrivateKey ; } return decryptContent ( content , opts ) ; } param redirectURL signUserOut ( redirectURL ? ) { this . store . deleteSessionData ( ) ; if ( redirectURL ) { if ( typeof location !== 'undefined' && location . href ) { location . href = redirectURL ; } } } }