@stacks/auth
Version:
Authentication for Stacks apps.
211 lines • 8.97 kB
JavaScript
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