blockstack
Version:
The Blockstack Javascript library for authentication, identity, and storage.
472 lines (425 loc) • 20.1 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.getTransitKey = getTransitKey;
exports.generateAndStoreTransitKey = generateAndStoreTransitKey;
exports.isUserSignedIn = isUserSignedIn;
exports.redirectToSignInWithAuthRequest = redirectToSignInWithAuthRequest;
exports.redirectToSignIn = redirectToSignIn;
exports.getAuthResponseToken = getAuthResponseToken;
exports.isSignInPending = isSignInPending;
exports.handlePendingSignIn = handlePendingSignIn;
exports.loadUserData = loadUserData;
exports.signUserOut = signUserOut;
var _queryString = require('query-string');
var _queryString2 = _interopRequireDefault(_queryString);
var _jsontokens = require('jsontokens');
var _index = require('./index');
var _utils = require('../utils');
var _index2 = require('../index');
var _errors = require('../errors');
var _authMessages = require('./authMessages');
var _authConstants = require('./authConstants');
var _storage = require('../storage');
var _profiles = require('../profiles');
var _logger = require('../logger');
var _config = require('../config');
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
var DEFAULT_PROFILE = {
'@type': 'Person',
'@context': 'http://schema.org'
/**
* Fetches the hex value of the transit private key from local storage.
* @return {String} the hex encoded private key
* @private
*/
};function getTransitKey() {
var transitKey = localStorage.getItem(_authConstants.BLOCKSTACK_APP_PRIVATE_KEY_LABEL);
return transitKey;
}
/**
* Generates a ECDSA keypair to
* use as the ephemeral app transit private key
* and stores the hex value of the private key in
* local storage.
* @return {String} the hex encoded private key
*/
function generateAndStoreTransitKey() {
var transitKey = (0, _index2.makeECPrivateKey)();
localStorage.setItem(_authConstants.BLOCKSTACK_APP_PRIVATE_KEY_LABEL, transitKey);
return transitKey;
}
/**
* Check if a user is currently signed in.
* @return {Boolean} `true` if the user is signed in, `false` if not.
*/
function isUserSignedIn() {
return !!window.localStorage.getItem(_authConstants.BLOCKSTACK_STORAGE_LABEL);
}
/**
* Detects if the native auth-browser is installed and is successfully
* launched via a custom protocol URI.
* @param {String} authRequest
* The encoded authRequest to be used as a query param in the custom URI.
* @param {String} successCallback
* The callback that is invoked when the protocol handler was detected.
* @param {String} failCallback
* The callback that is invoked when the protocol handler was not detected.
* @return {void}
*/
function detectProtocolLaunch(authRequest, successCallback, failCallback) {
// Create a unique ID used for this protocol detection attempt.
var echoReplyID = Math.random().toString(36).substr(2, 9);
var echoReplyKeyPrefix = 'echo-reply-';
var echoReplyKey = '' + echoReplyKeyPrefix + echoReplyID;
// Use localStorage as a reliable cross-window communication method.
// Create the storage entry to signal a protocol detection attempt for the
// next browser window to check.
window.localStorage.setItem(echoReplyKey, Date.now().toString());
var cleanUpLocalStorage = function cleanUpLocalStorage() {
try {
window.localStorage.removeItem(echoReplyKey);
// Also clear out any stale echo-reply keys older than 1 hour.
for (var i = 0; i < window.localStorage.length; i++) {
var storageKey = window.localStorage.key(i);
if (storageKey.startsWith(echoReplyKeyPrefix)) {
var storageValue = window.localStorage.getItem(storageKey);
if (storageValue === 'success' || Date.now() - parseInt(storageValue, 10) > 3600000) {
window.localStorage.removeItem(storageKey);
}
}
}
} catch (err) {
_logger.Logger.error('Exception cleaning up echo-reply entries in localStorage');
_logger.Logger.error(err);
}
};
var detectionTimeout = 1000;
var redirectToWebAuthTimer = 0;
var cancelWebAuthRedirectTimer = function cancelWebAuthRedirectTimer() {
if (redirectToWebAuthTimer) {
window.clearTimeout(redirectToWebAuthTimer);
redirectToWebAuthTimer = 0;
}
};
var startWebAuthRedirectTimer = function startWebAuthRedirectTimer() {
var timeout = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : detectionTimeout;
cancelWebAuthRedirectTimer();
redirectToWebAuthTimer = window.setTimeout(function () {
if (redirectToWebAuthTimer) {
cancelWebAuthRedirectTimer();
var nextFunc = void 0;
if (window.localStorage.getItem(echoReplyKey) === 'success') {
_logger.Logger.info('Protocol echo reply detected.');
nextFunc = successCallback;
} else {
_logger.Logger.info('Protocol handler not detected.');
nextFunc = failCallback;
}
failCallback = function failCallback() {};
successCallback = function successCallback() {};
cleanUpLocalStorage();
// Briefly wait since localStorage changes can
// sometimes be ignored when immediately redirected.
setTimeout(function () {
return nextFunc();
}, 100);
}
}, timeout);
};
startWebAuthRedirectTimer();
var inputPromptTracker = document.createElement('input');
inputPromptTracker.type = 'text';
// Prevent this element from inherited any css.
inputPromptTracker.style.all = 'initial';
// Setting display=none on an element prevents them from being focused/blurred.
// So hide the element using other properties..
inputPromptTracker.style.opacity = '0';
inputPromptTracker.style.filter = 'alpha(opacity=0)';
inputPromptTracker.style.height = '0';
inputPromptTracker.style.width = '0';
// If the the focus of a page element is immediately changed then this likely indicates
// the protocol handler is installed, and the browser is prompting the user if they want
// to open the application.
var inputBlurredFunc = function inputBlurredFunc() {
// Use a timeout of 100ms to ignore instant toggles between blur and focus.
// Browsers often perform an instant blur & focus when the protocol handler is working
// but not showing any browser prompts, so we want to ignore those instances.
var isRefocused = false;
inputPromptTracker.addEventListener('focus', function () {
isRefocused = true;
}, { once: true, capture: true });
setTimeout(function () {
if (redirectToWebAuthTimer && !isRefocused) {
_logger.Logger.info('Detected possible browser prompt for opening the protocol handler app.');
window.clearTimeout(redirectToWebAuthTimer);
inputPromptTracker.addEventListener('focus', function () {
if (redirectToWebAuthTimer) {
_logger.Logger.info('Possible browser prompt closed, restarting auth redirect timeout.');
startWebAuthRedirectTimer();
}
}, { once: true, capture: true });
}
}, 100);
};
inputPromptTracker.addEventListener('blur', inputBlurredFunc, { once: true, capture: true });
setTimeout(function () {
return inputPromptTracker.removeEventListener('blur', inputBlurredFunc);
}, 200);
// Flow complains without this check.
if (document.body) document.body.appendChild(inputPromptTracker);
inputPromptTracker.focus();
// Detect if document.visibility is immediately changed which is a strong
// indication that the protocol handler is working. We don't know for sure and
// can't predict future browser changes, so only increase the redirect timeout.
// This reduces the probability of a false-negative (where local auth works, but
// the original page was redirect to web auth because something took too long),
var pageVisibilityChanged = function pageVisibilityChanged() {
if (document.hidden && redirectToWebAuthTimer) {
_logger.Logger.info('Detected immediate page visibility change (protocol handler probably working).');
startWebAuthRedirectTimer(3000);
}
};
document.addEventListener('visibilitychange', pageVisibilityChanged, { once: true, capture: true });
setTimeout(function () {
return document.removeEventListener('visibilitychange', pageVisibilityChanged);
}, 500);
// Listen for the custom protocol echo reply via localStorage update event.
window.addEventListener('storage', function replyEventListener(event) {
if (event.key === echoReplyKey && window.localStorage.getItem(echoReplyKey) === 'success') {
// Custom protocol worked, cancel the web auth redirect timer.
cancelWebAuthRedirectTimer();
inputPromptTracker.removeEventListener('blur', inputBlurredFunc);
_logger.Logger.info('Protocol echo reply detected from localStorage event.');
// Clean up event listener and localStorage.
window.removeEventListener('storage', replyEventListener);
var nextFunc = successCallback;
successCallback = function successCallback() {};
failCallback = function failCallback() {};
cleanUpLocalStorage();
// Briefly wait since localStorage changes can sometimes
// be ignored when immediately redirected.
setTimeout(function () {
return nextFunc();
}, 100);
}
}, false);
// Use iframe technique for launching the protocol URI rather than setting `window.location`.
// This method prevents browsers like Safari, Opera, Firefox from showing error prompts
// about unknown protocol handler when app is not installed, and avoids an empty
// browser tab when the app is installed.
_logger.Logger.info('Attempting protocol launch via iframe injection.');
var locationSrc = _utils.BLOCKSTACK_HANDLER + ':' + authRequest + '&echo=' + echoReplyID;
var iframe = document.createElement('iframe');
iframe.style.all = 'initial';
iframe.style.display = 'none';
iframe.src = locationSrc;
// Flow complains without this check.
if (document.body) {
document.body.appendChild(iframe);
} else {
_logger.Logger.error('document.body is null when attempting iframe injection for protoocol URI launch');
}
}
/**
* Redirects the user to the Blockstack browser to approve the sign in request
* given.
*
* The user is redirected to the `blockstackIDHost` if the `blockstack:`
* protocol handler is not detected. Please note that the protocol handler detection
* does not work on all browsers.
* @param {String} authRequest - the authentication request generated by `makeAuthRequest`
* @param {String} blockstackIDHost - the URL to redirect the user to if the blockstack
* protocol handler is not detected
* @return {void}
*/
function redirectToSignInWithAuthRequest() {
var authRequest = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : (0, _index.makeAuthRequest)();
var blockstackIDHost = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : _authConstants.DEFAULT_BLOCKSTACK_HOST;
var httpsURI = blockstackIDHost + '?authRequest=' + authRequest;
// If they're on a mobile OS, always redirect them to HTTPS site
if (/Android|webOS|iPhone|iPad|iPod|Opera Mini/i.test(navigator.userAgent)) {
_logger.Logger.info('detected mobile OS, sending to https');
window.location = httpsURI;
return;
}
function successCallback() {
_logger.Logger.info('protocol handler detected');
// The detection function should open the link for us
}
function failCallback() {
_logger.Logger.warn('protocol handler not detected');
window.location = httpsURI;
}
detectProtocolLaunch(authRequest, successCallback, failCallback);
}
/**
* Generates an authentication request and redirects the user to the Blockstack
* browser to approve the sign in request.
*
* Please note that this requires that the web browser properly handles the
* `blockstack:` URL protocol handler.
*
* Most applications should use this
* method for sign in unless they require more fine grained control over how the
* authentication request is generated. If your app falls into this category,
* use `makeAuthRequest` and `redirectToSignInWithAuthRequest` to build your own sign in process.
*
* @param {String} [redirectURI=`${window.location.origin}/`]
* The location to which the identity provider will redirect the user after
* the user approves sign in.
* @param {String} [manifestURI=`${window.location.origin}/manifest.json`]
* Location of the manifest file.
* @param {Array} [scopes=DEFAULT_SCOPE] Defaults to requesting write access to
* this app's data store.
* An array of strings indicating which permissions this app is requesting.
* @return {void}
*/
function redirectToSignIn() {
var redirectURI = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : window.location.origin + '/';
var manifestURI = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : window.location.origin + '/manifest.json';
var scopes = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : _authConstants.DEFAULT_SCOPE;
var authRequest = (0, _index.makeAuthRequest)(generateAndStoreTransitKey(), redirectURI, manifestURI, scopes);
redirectToSignInWithAuthRequest(authRequest);
}
/**
* Retrieve the authentication token from the URL query
* @return {String} the authentication token if it exists otherwise `null`
*/
function getAuthResponseToken() {
var queryDict = _queryString2.default.parse(location.search);
return queryDict.authResponse ? queryDict.authResponse : '';
}
/**
* Check if there is a authentication request that hasn't been handled.
* @return {Boolean} `true` if there is a pending sign in, otherwise `false`
*/
function isSignInPending() {
return !!getAuthResponseToken();
}
/**
* Try to process any pending sign in request by returning a `Promise` that resolves
* to the user data object if the sign in succeeds.
*
* @param {String} nameLookupURL - the endpoint against which to verify public
* keys match claimed username
* @param {String} authResponseToken - the signed authentication response token
* @param {String} transitKey - the transit private key that corresponds to the transit public key
* that was provided in the authentication request
* @return {Promise} that resolves to the user data object if successful and rejects
* if handling the sign in request fails or there was no pending sign in request.
*/
function handlePendingSignIn() {
var nameLookupURL = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : '';
var authResponseToken = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : getAuthResponseToken();
var transitKey = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : getTransitKey();
if (!nameLookupURL) {
var tokenPayload = (0, _jsontokens.decodeToken)(authResponseToken).payload;
if ((0, _utils.isLaterVersion)(tokenPayload.version, '1.3.0') && tokenPayload.blockstackAPIUrl !== null && tokenPayload.blockstackAPIUrl !== undefined) {
// override globally
_logger.Logger.info('Overriding ' + _config.config.network.blockstackAPIUrl + ' ' + ('with ' + tokenPayload.blockstackAPIUrl));
_config.config.network.blockstackAPIUrl = tokenPayload.blockstackAPIUrl;
nameLookupURL = tokenPayload.blockstackAPIUrl + '/v1/names';
}
nameLookupURL = _config.config.network.blockstackAPIUrl + '/v1/names/';
}
return (0, _index.verifyAuthResponse)(authResponseToken, nameLookupURL).then(function (isValid) {
if (!isValid) {
throw new _errors.LoginFailedError('Invalid authentication response.');
}
var tokenPayload = (0, _jsontokens.decodeToken)(authResponseToken).payload;
// TODO: real version handling
var appPrivateKey = tokenPayload.private_key;
var coreSessionToken = tokenPayload.core_token;
if ((0, _utils.isLaterVersion)(tokenPayload.version, '1.1.0')) {
if (transitKey !== undefined && transitKey != null) {
if (tokenPayload.private_key !== undefined && tokenPayload.private_key !== null) {
try {
appPrivateKey = (0, _authMessages.decryptPrivateKey)(transitKey, tokenPayload.private_key);
} catch (e) {
_logger.Logger.warn('Failed decryption of appPrivateKey, will try to use as given');
try {
(0, _utils.hexStringToECPair)(tokenPayload.private_key);
} catch (ecPairError) {
throw new _errors.LoginFailedError('Failed decrypting appPrivateKey. Usually means' + ' that the transit key has changed during login.');
}
}
}
if (coreSessionToken !== undefined && coreSessionToken !== null) {
try {
coreSessionToken = (0, _authMessages.decryptPrivateKey)(transitKey, coreSessionToken);
} catch (e) {
_logger.Logger.info('Failed decryption of coreSessionToken, will try to use as given');
}
}
} else {
throw new _errors.LoginFailedError('Authenticating with protocol > 1.1.0 requires transit' + ' key, and none found.');
}
}
var hubUrl = _authConstants.BLOCKSTACK_DEFAULT_GAIA_HUB_URL;
var gaiaAssociationToken = void 0;
if ((0, _utils.isLaterVersion)(tokenPayload.version, '1.2.0') && tokenPayload.hubUrl !== null && tokenPayload.hubUrl !== undefined) {
hubUrl = tokenPayload.hubUrl;
}
if ((0, _utils.isLaterVersion)(tokenPayload.version, '1.3.0') && tokenPayload.associationToken !== null && tokenPayload.associationToken !== undefined) {
gaiaAssociationToken = tokenPayload.associationToken;
}
var userData = {
username: tokenPayload.username,
profile: tokenPayload.profile,
decentralizedID: tokenPayload.iss,
identityAddress: (0, _index2.getAddressFromDID)(tokenPayload.iss),
appPrivateKey: appPrivateKey,
coreSessionToken: coreSessionToken,
authResponseToken: authResponseToken,
hubUrl: hubUrl,
gaiaAssociationToken: gaiaAssociationToken
};
var profileURL = tokenPayload.profile_url;
if ((userData.profile === null || userData.profile === undefined) && profileURL !== undefined && profileURL !== null) {
return fetch(profileURL).then(function (response) {
if (!response.ok) {
// return blank profile if we fail to fetch
userData.profile = Object.assign({}, DEFAULT_PROFILE);
window.localStorage.setItem(_authConstants.BLOCKSTACK_STORAGE_LABEL, JSON.stringify(userData));
return userData;
} else {
return response.text().then(function (responseText) {
return JSON.parse(responseText);
}).then(function (wrappedProfile) {
return (0, _profiles.extractProfile)(wrappedProfile[0].token);
}).then(function (profile) {
userData.profile = profile;
window.localStorage.setItem(_authConstants.BLOCKSTACK_STORAGE_LABEL, JSON.stringify(userData));
return userData;
});
}
});
} else {
userData.profile = tokenPayload.profile;
window.localStorage.setItem(_authConstants.BLOCKSTACK_STORAGE_LABEL, JSON.stringify(userData));
return userData;
}
});
}
/**
* Retrieves the user data object. The user's profile is stored in the key `profile`.
* @return {Object} User data object.
*/
function loadUserData() {
return JSON.parse(window.localStorage.getItem(_authConstants.BLOCKSTACK_STORAGE_LABEL));
}
/**
* Sign the user out and optionally redirect to given location.
* @param {String} [redirectURL=null] Location to redirect user to after sign out.
* @return {void}
*/
function signUserOut() {
var redirectURL = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : null;
window.localStorage.removeItem(_authConstants.BLOCKSTACK_STORAGE_LABEL);
window.localStorage.removeItem(_storage.BLOCKSTACK_GAIA_HUB_LABEL);
if (redirectURL !== null) {
window.location = redirectURL;
}
}