UNPKG

@stacks/auth

Version:

Authentication for Stacks apps.

211 lines • 8.97 kB
import { AppConfig } from './appConfig'; import { InstanceDataStore, LocalStorageStore } from './sessionStore'; import { decodeToken } from 'jsontokens'; import { verifyAuthResponse } from './verification'; import * as authMessages from './messages'; import { utils } from '@noble/secp256k1'; import { decryptContent, encryptContent } from '@stacks/encryption'; import { getAddressFromDID } from './dids'; import { createFetchFn, GAIA_URL, getGlobalObject, HIRO_MAINNET_URL, InvalidStateError, isLaterVersion, Logger, LoginFailedError, MissingParameterError, nextHour, } from '@stacks/common'; import { extractProfile } from '@stacks/profile'; import { DEFAULT_PROFILE } from './constants'; import { protocolEchoReplyDetection } from './protocolEchoDetection'; export class UserSession { 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(); } } makeAuthRequestToken(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.makeAuthRequestToken(transitKey, redirectURI, manifestURI, scopes, appDomain, expiresAt, extraParams); } generateAndStoreTransitKey() { const sessionData = this.store.getSessionData(); const transitKey = authMessages.generateTransitKey(); sessionData.transitKey = transitKey; this.store.setSessionData(sessionData); return transitKey; } getAuthResponseToken() { const search = getGlobalObject('location', { throwIfUnavailable: true, usageDesc: 'getAuthResponseToken', })?.search; const params = new URLSearchParams(search); return params.get('authResponse') ?? ''; } 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(); } isUserSignedIn() { return !!this.store.getSessionData().userData; } async handlePendingSignIn(authResponseToken = this.getAuthResponseToken(), fetchFn = createFetchFn()) { 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) { coreNode = HIRO_MAINNET_URL; } const tokenPayload = decodeToken(authResponseToken).payload; if (typeof tokenPayload === 'string') { throw new Error('Unexpected token payload type of string'); } const isValid = await verifyAuthResponse(authResponseToken); if (!isValid) { throw new LoginFailedError('Invalid authentication response.'); } let appPrivateKey = tokenPayload.private_key; let coreSessionToken = tokenPayload.core_token; if (isLaterVersion(tokenPayload.version, '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)); } catch (e) { Logger.warn('Failed decryption of appPrivateKey, will try to use as given'); if (!utils.isValidPrivateKey(tokenPayload.private_key)) { 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)); } 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 = GAIA_URL; let gaiaAssociationToken; if (isLaterVersion(tokenPayload.version, '1.2.0') && tokenPayload.hubUrl !== null && tokenPayload.hubUrl !== undefined) { hubUrl = tokenPayload.hubUrl; } if (isLaterVersion(tokenPayload.version, '1.3.0') && tokenPayload.associationToken !== null && tokenPayload.associationToken !== undefined) { gaiaAssociationToken = tokenPayload.associationToken; } const userData = { profile: tokenPayload.profile, email: tokenPayload.email, decentralizedID: tokenPayload.iss, identityAddress: getAddressFromDID(tokenPayload.iss), appPrivateKey, coreSessionToken, authResponseToken, hubUrl, appPrivateKeyFromWalletSalt: tokenPayload.appPrivateKeyFromWalletSalt, coreNode: tokenPayload.blockstackAPIUrl, gaiaAssociationToken, }; const profileURL = tokenPayload.profile_url; if (!userData.profile && profileURL) { const response = await fetchFn(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; } loadUserData() { const userData = this.store.getSessionData().userData; if (!userData) { throw new InvalidStateError('No user data found. Did the user sign in?'); } return userData; } encryptContent(content, options) { const opts = Object.assign({}, options); if (!opts.privateKey) { opts.privateKey = this.loadUserData().appPrivateKey; } return encryptContent(content, opts); } decryptContent(content, options) { const opts = Object.assign({}, options); if (!opts.privateKey) { opts.privateKey = this.loadUserData().appPrivateKey; } return decryptContent(content, opts); } signUserOut(redirectURL) { this.store.deleteSessionData(); if (redirectURL) { if (typeof location !== 'undefined' && location.href) { location.href = redirectURL; } } } } UserSession.prototype.makeAuthRequest = UserSession.prototype.makeAuthRequestToken; //# sourceMappingURL=userSession.js.map