blockstack
Version:
The Blockstack Javascript library for authentication, identity, and storage.
1,273 lines (1,150 loc) • 3.05 MB
JavaScript
(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.blockstack = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){
'use strict';
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;
}
}
},{"../config":8,"../errors":11,"../index":12,"../logger":14,"../profiles":22,"../storage":45,"../utils":46,"./authConstants":2,"./authMessages":3,"./index":7,"jsontokens":383,"query-string":449}],2:[function(require,module,exports){
'use strict';
Object.defineProperty(exports, "__esModule", {
value: true
});
var BLOCKSTACK_HANDLER = exports.BLOCKSTACK_HANDLER = 'blockstack';
var BLOCKSTACK_STORAGE_LABEL = exports.BLOCKSTACK_STORAGE_LABEL = 'blockstack';
var DEFAULT_BLOCKSTACK_HOST = exports.DEFAULT_BLOCKSTACK_HOST = 'https://browser.blockstack.org/auth';
var DEFAULT_SCOPE = exports.DEFAULT_SCOPE = ['store_write'];
var BLOCKSTACK_APP_PRIVATE_KEY_LABEL = exports.BLOCKSTACK_APP_PRIVATE_KEY_LABEL = 'blockstack-transit-private-key';
var BLOCKSTACK_DEFAULT_GAIA_HUB_URL = exports.BLOCKSTACK_DEFAULT_GAIA_HUB_URL = 'https://hub.blockstack.org';
},{}],3:[function(require,module,exports){
(function (Buffer){
'use strict';
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.makeAuthRequest = makeAuthRequest;
exports.encryptPrivateKey = encryptPrivateKey;
exports.decryptPrivateKey = decryptPrivateKey;
exports.makeAuthResponse = makeAuthResponse;
require('cross-fetch/polyfill');
var _jsontokens = require('jsontokens');
var _index = require('../index');
var _authConstants = require('./authConstants');
var _encryption = require('../encryption');
var _logger = require('../logger');
var VERSION = '1.3.1';
/**
* Generates an authentication request that can be sent to the Blockstack
* browser for the user to approve sign in. This authentication request can
* then be used for sign in by passing it to the `redirectToSignInWithAuthRequest`
* method.
*
* *Note: This method should only be used if you want to roll your own authentication
* flow. Typically you'd use `redirectToSignIn` which takes care of this
* under the hood.*
*
* @param {String} [transitPrivateKey=generateAndStoreTransitKey()] - hex encoded transit
* private key
* @param {String} redirectURI - location to redirect user to after sign in approval
* @param {String} manifestURI - location of this app's manifest file
* @param {Array<String>} scopes - the permissions this app is requesting
* @param {String} appDomain - the origin of this app
* @param {Number} expiresAt - the time at which this request is no longer valid
* @param {Object} extraParams - Any extra parameters you'd like to pass to the authenticator.
* Use this to pass options that aren't part of the Blockstack auth spec, but might be supported
* by special authenticators.
* @return {String} the authentication request
*/
function makeAuthRequest() {
var transitPrivateKey = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : (0, _index.generateAndStoreTransitKey)();
var redirectURI = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : window.location.origin + '/';
var manifestURI = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : window.location.origin + '/manifest.json';
var scopes = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : _authConstants.DEFAULT_SCOPE;
var appDomain = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : window.location.origin;
var expiresAt = arguments.length > 5 && arguments[5] !== undefined ? arguments[5] : (0, _index.nextHour)().getTime();
var extraParams = arguments.length > 6 && arguments[6] !== undefined ? arguments[6] : {};
/* Create the payload */
var payload = Object.assign({}, extraParams, {
jti: (0, _index.makeUUID4)(),
iat: Math.floor(new Date().getTime() / 1000), // JWT times are in seconds
exp: Math.floor(expiresAt / 1000), // JWT times are in seconds
iss: null,
public_keys: [],
domain_name: appDomain,
manifest_uri: manifestURI,
redirect_uri: redirectURI,
version: VERSION,
do_not_include_profile: true,
supports_hub_url: true,
scopes: scopes
});
_logger.Logger.info('blockstack.js: generating v' + VERSION + ' auth request');
/* Convert the private key to a public key to an issuer */
var publicKey = _jsontokens.SECP256K1Client.derivePublicKey(transitPrivateKey);
payload.public_keys = [publicKey];
var address = (0, _index.publicKeyToAddress)(publicKey);
payload.iss = (0, _index.makeDIDFromAddress)(address);
/* Sign and return the token */
var tokenSigner = new _jsontokens.TokenSigner('ES256k', transitPrivateKey);
var token = tokenSigner.sign(payload);
return token;
}
/**
* Encrypts the private key for decryption by the given
* public key.
* @param {String} publicKey [description]
* @param {String} privateKey [description]
* @return {String} hex encoded ciphertext
* @private
*/
function encryptPrivateKey(publicKey, privateKey) {
var encryptedObj = (0, _encryption.encryptECIES)(publicKey, privateKey);
var encryptedJSON = JSON.stringify(encryptedObj);
return new Buffer(encryptedJSON).toString('hex');
}
/**
* Decrypts the hex encrypted private key
* @param {String} privateKey the private key corresponding to the public
* key for which the ciphertext was encrypted
* @param {String} hexedEncrypted the ciphertext
* @return {String} the decrypted private key
* @throws {Error} if unable to decrypt
*
* @private
*/
function decryptPrivateKey(privateKey, hexedEncrypted) {
var unhexedString = new Buffer(hexedEncrypted, 'hex').toString();
var encryptedObj = JSON.parse(unhexedString);
var decrypted = (0, _encryption.decryptECIES)(privateKey, encryptedObj);
if (typeof decrypted !== 'string') {
throw new Error('Unable to correctly decrypt private key');
} else {
return decrypted;
}
}
/**
* Generates a signed authentication response token for an app. This
* token is sent back to apps which use contents to access the
* resources and data requested by the app.
*
* @param {String} privateKey the identity key of the Blockstack ID generating
* the authentication response
* @param {Object} profile the profile object for the Blockstack ID
* @param {String} username the username of the Blockstack ID if any, otherwise `null`
* @param {AuthMetadata} metadata an object containing metadata sent as part of the authentication
* response including `email` if requested and available and a URL to the profile
* @param {String} coreToken core session token when responding to a legacy auth request
* or `null` for current direct to gaia authentication requests
* @param {String} appPrivateKey the application private key. This private key is
* unique and specific for every Blockstack ID and application combination.
* @param {Number} expiresAt an integer in the same format as
* `new Date().getTime()`, milliseconds since the Unix epoch
* @param {String} transitPublicKey the public key provide by the app
* in its authentication request with which secrets will be encrypted
* @param {String} hubUrl URL to the write path of the user's Gaia hub
* @param {String} blockstackAPIUrl URL to the API endpoint to use
* @param {String} associationToken JWT that binds the app key to the identity key
* @return {String} signed and encoded authentication response token
* @private
*/
function makeAuthResponse(privateKey) {
var profile = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
var username = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : null;
var metadata = arguments[3];
var coreToken = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : null;
var appPrivateKey = arguments.length > 5 && arguments[5] !== undefined ? arguments[5] : null;
var expiresAt = arguments.length > 6 && arguments[6] !== undefined ? arguments[6] : (0, _index.nextMonth)().getTime();
var transitPublicKey = arguments.length > 7 && arguments[7] !== undefined ? arguments[7] : null;
var hubUrl = arguments.length > 8 && arguments[8] !== undefined ? arguments[8] : null;
var blockstackAPIUrl = arguments.length > 9 && arguments[9] !== undefined ? arguments[9] : null;
var associationToken = arguments.length > 10 && arguments[10] !== undefined ? arguments[10] : null;
/* Convert the private key to a public key to an issuer */
var publicKey = _jsontokens.SECP256K1Client.derivePublicKey(privateKey);
var address = (0, _index.publicKeyToAddress)(publicKey);
/* See if we should encrypt with the transit key */
var privateKeyPayload = appPrivateKey;
var coreTokenPayload = coreToken;
var additionalProperties = {};
if (appPrivateKey !== undefined && appPrivateKey !== null) {
_logger.Logger.info('blockstack.js: generating v' + VERSION + ' auth response');
if (transitPublicKey !== undefined && transitPublicKey !== null) {
privateKeyPayload = encryptPrivateKey(transitPublicKey, appPrivateKey);
if (coreToken !== undefined && coreToken !== null) {
coreTokenPayload = encryptPrivateKey(transitPublicKey, coreToken);
}
}
additionalProperties = {
email: metadata.email ? metadata.email : null,
profile_url: metadata.profileUrl ? metadata.profileUrl : null,
hubUrl: hubUrl,
blockstackAPIUrl: blockstackAPIUrl,
associationToken: associationToken,
version: VERSION
};
} else {
_logger.Logger.info('blockstack.js: generating legacy auth response');
}
/* Create the payload */
var payload = Object.assign({}, {
jti: (0, _index.makeUUID4)(),
iat: Math.floor(new Date().getTime() / 1000), // JWT times are in seconds
exp: Math.floor(expiresAt / 1000), // JWT times are in seconds
iss: (0, _index.makeDIDFromAddress)(address),
private_key: privateKeyPayload,
public_keys: [publicKey],
profile: profile,
username: username,
core_token: coreTokenPayload
}, additionalProperties);
/* Sign and return the token */
var tokenSigner = new _jsontokens.TokenSigner('ES256k', privateKey);
return tokenSigner.sign(payload);
}
}).call(this,require("buffer").Buffer)
},{"../encryption":10,"../index":12,"../logger":14,"./authConstants":2,"buffer":179,"cross-fetch/polyfill":283,"jsontokens":383}],4:[function(require,module,exports){
'use strict';
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.getAuthRequestFromURL = getAuthRequestFromURL;
exports.fetchAppManifest = fetchAppManifest;
exports.redirectUserToApp = redirectUserToApp;
var _queryString = require('query-string');
var _queryString2 = _interopRequireDefault(_queryString);
var _jsontokens = require('jsontokens');
var _index = require('../index');
var _utils = require('../utils');
var _logger = require('../logger');
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
/**
* Retrieves the authentication request from the query string
* @return {String|null} the authentication request or `null` if
* the query string parameter `authRequest` is not found
* @private
*/
function getAuthRequestFromURL() {
var queryDict = _queryString2.default.parse(location.search);
if (queryDict.authRequest !== null && queryDict.authRequest !== undefined) {
return queryDict.authRequest.split(_utils.BLOCKSTACK_HANDLER + ':').join('');
} else {
return null;
}
}
/**
* Fetches the contents of the manifest file specified in the authentication request
*
* @param {String} authRequest encoded and signed authentication request
* @return {Promise<Object|String>} Returns a `Promise` that resolves to the JSON
* object manifest file unless there's an error in which case rejects with an error
* message.
* @private
*/
function fetchAppManifest(authRequest) {
return new Promise(function (resolve, reject) {
if (!authRequest) {
reject('Invalid auth request');
} else {
var payload = (0, _jsontokens.decodeToken)(authRequest).payload;
var manifestURI = payload.manifest_uri;
try {
_logger.Logger.debug('Fetching manifest from ' + manifestURI);
fetch(manifestURI).then(function (response) {
return response.text();
}).then(function (responseText) {
return JSON.parse(responseText);
}).then(function (responseJSON) {
resolve(responseJSON);
}).catch(function (e) {
_logger.Logger.debug(e.stack);
reject('Could not fetch manifest.json');
});
} catch (e) {
_logger.Logger.debug(e.stack);
reject('Could not fetch manifest.json');
}
}
});
}
/**
* Redirect the user's browser to the app using the `redirect_uri`
* specified in the authentication request, passing the authentication
* response token as a query parameter.
*
* @param {String} authRequest encoded and signed authentication request token
* @param {String} authResponse encoded and signed authentication response token
* @return {void}
* @throws {Error} if there is no redirect uri
* @private
*/
function redirectUserToApp(authRequest, authResponse) {
var payload = (0, _jsontokens.decodeToken)(authRequest).payload;
var redirectURI = payload.redirect_uri;
_logger.Logger.debug(redirectURI);
if (redirectURI) {
redirectURI = (0, _index.updateQueryStringParameter)(redirectURI, 'authResponse', authResponse);
} else {
throw new Error('Invalid redirect URI');
}
window.location = redirectURI;
}
},{"../index":12,"../logger":14,"../utils":46,"jsontokens":383,"query-string":449}],5:[function(require,module,exports){
'use strict';
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.makeCoreSessionRequest = makeCoreSessionRequest;
exports.sendCoreSessionRequest = sendCoreSessionRequest;
exports.getCoreSession = getCoreSession;
var _jsontokens = require('jsontokens');
require('cross-fetch/polyfill');
/**
* Create an authentication token to be sent to the Core API server
* in order to generate a Core session JWT.
*
* @param {String} appDomain The unique application identifier (e.g. foo.app, www.foo.com, etc).
* @param {Array} appMethods The list of API methods this application will need.
* @param {String} appPrivateKey The application-specific private key
* @param {String|null} blockchainID This is the blockchain ID of the requester
* @param {String} thisDevice Identifier of the current device
*
* @return {String} a JWT signed by the app's private key
* @deprecated
* @private
*/
function makeCoreSessionRequest(appDomain, appMethods, appPrivateKey) {
var blockchainID = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : null;
var thisDevice = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : null;
if (thisDevice === null) {
thisDevice = '.default';
}
var appPublicKey = _jsontokens.SECP256K1Client.derivePublicKey(appPrivateKey);
var appPublicKeys = [{
public_key: appPublicKey,
device_id: thisDevice
}];
var authBody = {
version: 1,
blockchain_id: blockchainID,
app_private_key: appPrivateKey,
app_domain: appDomain,
methods: appMethods,
app_public_keys: appPublicKeys,
device_id: thisDevice
// make token
};var tokenSigner = new _jsontokens.TokenSigner('ES256k', appPrivateKey);
var token = tokenSigner.sign(authBody);
return token;
}
/**
* Send Core a request for a session token.
*
* @param {String} coreHost host name of the core node
* @param {Number} corePort port number of the core node
* @param {String} coreAuthRequest a signed JWT encoding the authentication request
* @param {String} apiPassword the API password for Core
*
* @return {Promise} the resolves to a JWT signed with the Core API server's private key
* that authorizes the bearer to carry out the requested operations and rejects
* with an error message otherwise
* @deprecated
* @private
*/
function sendCoreSessionRequest(coreHost, corePort, coreAuthRequest, apiPassword) {
return new Promise(function (resolve, reject) {
if (!apiPassword) {
reject('Missing API password');
return null;
}
var options = {
headers: {
Authorization: 'bearer ' + apiPassword
}
};
var url = 'http://' + coreHost + ':' + corePort + '/v1/auth?authRequest=' + coreAuthRequest;
return fetch(url, options).then(function (response) {
if (!response.ok) {
reject('HTTP status not OK');
throw new Error('HTTP status not OK');
}
return response.text();
}).then(function (responseText) {
return JSON.parse(responseText);
}).then(function (responseJson) {
var token = responseJson.token;
if (!token) {
reject('Failed to get Core session token');
return null;
}
resolve(token);
return token;
}).catch(function (error) {
console.error(error);
reject('Invalid Core response: not JSON');
});
});
}
/**
* Get a core session token. Generate an auth request, sign it, send it to Core,
* and get back a session token.
*
* @param {String} coreHost Core API server's hostname
* @param {Number} corePort Core API server's port number
* @param {String} apiPassword core api password
* @param {String} appPrivateKey Application's private key
* @param {String} blockchainId blockchain ID of the user signing in.
* `null` if user has no blockchain ID
* @param {String} authRequest authentication request token
* @param {String} deviceId identifier for the current device
*
* @return {Promise} a Promise that resolves to a Core session token or rejects
* with an error message.
* @deprecated
* @private
*/
function getCoreSession(coreHost, corePort, apiPassword, appPrivateKey) {
var blockchainId = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : null;
var authRequest = arguments.length > 5 && arguments[5] !== undefined ? arguments[5] : null;
var deviceId = arguments.length > 6 && arguments[6] !== undefined ? arguments[6] : '0';
if (!authRequest) {
return Promise.reject('No authRequest provided');
}
var payload = null;
var authRequestObject = null;
try {
authRequestObject = (0, _jsontokens.decodeToken)(authRequest);
if (!authRequestObject) {
return Promise.reject('Invalid authRequest in URL query string');
}
if (!authRequestObject.payload) {
return Promise.reject('Invalid authRequest in URL query string');
}
payload = authRequestObject.payload;
} catch (e) {
console.error(e.stack);
return Promise.reject('Failed to parse authRequest in URL');
}
var appDomain = payload.domain_name;
if (!appDomain) {
return Promise.reject('No domain_name in authRequest');
}
var appMethods = payload.scopes;
var coreAuthRequest = makeCoreSessionRequest(appDomain, appMethods, appPrivateKey, blockchainId, deviceId);
return sendCoreSessionRequest(coreHost, corePort, coreAuthRequest, apiPassword);
}
},{"cross-fetch/polyfill":283,"jsontokens":383}],6:[function(require,module,exports){
'use strict';
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.doSignaturesMatchPublicKeys = doSignaturesMatchPublicKeys;
exports.doPublicKeysMatchIssuer = doPublicKeysMatchIssuer;
exports.doPublicKeysMatchUsername = doPublicKeysMatchUsername;
exports.isIssuanceDateValid = isIssuanceDateValid;
exports.isExpirationDateValid = isExpirationDateValid;
exports.isManifestUriValid = isManifestUriValid;
exports.isRedirectUriValid = isRedirectUriValid;
exports.verifyAuthRequest = verifyAuthRequest;
exports.verifyAuthRequestAndLoadManifest = verifyAuthRequestAndLoadManifest;
exports.verifyAuthResponse = verifyAuthResponse;
var _jsontokens = require('jsontokens');
var _index = require('../index');
/**
* Checks if the ES256k signature on passed `token` match the claimed public key
* in the payload key `public_keys`.
*
* @param {String} token encoded and signed authentication token
* @return {Boolean} Returns `true` if the signature matches the claimed public key
* @throws {Error} if `token` contains multiple public keys
* @private
*/
function doSignaturesMatchPublicKeys(token) {
var payload = (0, _jsontokens.decodeToken)(token).payload;
var publicKeys = payload.public_keys;
if (publicKeys.length === 1) {
var publicKey = publicKeys[0];
try {
var tokenVerifier = new _jsontokens.TokenVerifier('ES256k', publicKey);
var signatureVerified = tokenVerifier.verify(token);
if (signatureVerified) {
return true;
} else {
return false;
}
} catch (e) {
return false;
}
} else {
throw new Error('Multiple public keys are not supported');
}
}
/**
* Makes sure that the identity address portion of
* the decentralized identifier passed in the issuer `iss`
* key of the token matches the public key
*
* @param {String} token encoded and signed authentication token
* @return {Boolean} if the identity address and public keys match
* @throws {Error} if ` token` has multiple public keys
* @private
*/
function doPublicKeysMatchIssuer(token) {
var payload = (0, _jsontokens.decodeToken)(token).payload;
var publicKeys = payload.public_keys;
var addressFromIssuer = (0, _index.getAddressFromDID)(payload.iss);
if (publicKeys.length === 1) {
var addressFromPublicKeys = (0, _index.publicKeyToAddress)(publicKeys[0]);
if (addressFromPublicKeys === addressFromIssuer) {
return true;
}
} else {
throw new Error('Multiple public keys are not supported');
}
return false;
}
/**
* Looks up the identity address that owns the claimed username
* in `token` using the lookup endpoint provided in `nameLookupURL`
* to determine if the username is owned by the identity address
* that matches the claimed public key
*
* @param {String} token encoded and signed authentication token
* @param {String} nameLookupURL a URL to the name lookup endpoint of the Blockstack Core API
* @return {Promise<Boolean>} returns a `Promise` that resolves to
* `true` if the username is owned by the public key, otherwise the
* `Promise` resolves to `false`
* @private
*/
function doPublicKeysMatchUsername(token, nameLookupURL) {
return new Promise(function (resolve) {
var payload = (0, _jsontokens.decodeToken)(token).payload;
if (!payload.username) {
resolve(true);
return;
}
if (payload.username === null) {
resolve(true);
return;
}
if (nameLookupURL === null) {
resolve(false);
return;
}
var username = payload.username;
var url = nameLookupURL.replace(/\/$/, '') + '/' + username;
try {
fetch(url).then(function (response) {
return response.text();
}).then(function (responseText) {
return JSON.parse(responseText);
}).then(function (responseJSON) {
if (responseJSON.hasOwnProperty('address')) {
var nameOwningAddress = responseJSON.address;
var addressFromIssuer = (0, _index.getAddressFromDID)(payload.iss);
if (nameOwningAddress === addressFromIssuer) {
resolve(true);
} else {
resolve(false);
}
} else {
resolve(false);
}
}).catch(function () {
resolve(false);
});
} catch (e) {
resolve(false);
}
});
}
/**
* Checks if the if the token issuance time and date is after the
* current time and date.
*
* @param {String} token encoded and signed authentication token
* @return {Boolean} `true` if the token was issued after the current time,
* otherwise returns `false`
* @private
*/
function isIssuanceDateValid(token) {
var payload = (0, _jsontokens.decodeToken)(token).payload;
if (payload.iat) {
if (typeof payload.iat !== 'number') {
return false;
}
var issuedAt = new Date(payload.iat * 1000); // JWT times are in seconds
if (new Date().getTime() < issuedAt.getTime()) {
return false;
} else {
return true;
}
} else {
return true;
}
}
/**
* Checks if the expiration date of the `token` is before the current time
* @param {String} token encoded and signed authentication token
* @return {Boolean} `true` if the `token` has not yet expired, `false`
* if the `token` has expired
*
* @private
*/
function isExpirationDateValid(token) {
var payload = (0, _jsontokens.decodeToken)(token).payload;
if (payload.exp) {
if (typeof payload.exp !== 'number') {
return false;
}
var expiresAt = new Date(payload.exp * 1000); // JWT times are in seconds
if (new Date().getTime() > expiresAt.getTime()) {
return false;
} else {
return true;
}
} else {
return true;
}
}
/**
* Makes sure the `manifest_uri` is a same origin absolute URL.
* @param {String} token encoded and signed authentication token
* @return {Boolean} `true` if valid, otherwise `false`
* @private
*/
function isManifestUriValid(token) {
var payload = (0, _jsontokens.decodeToken)(token).payload;
return (0, _index.isSameOriginAbsoluteUrl)(payload.domain_name, payload.manifest_uri);
}
/**
* Makes sure the `redirect_uri` is a same origin absolute URL.
* @param {String} token encoded and signed authentication token
* @return {Boolean} `true` if valid, otherwise `false`
* @private
*/
function isRedirectUriValid(token) {
var payload = (0, _jsontokens.decodeToken)(token).payload;
return (0, _index.isSameOriginAbsoluteUrl)(payload.domain_name, payload.redirect_uri);
}
/**
* Verify authentication request is valid. This function performs a number
* of checks on the authentication request token:
* * Checks that `token` has a valid issuance date & is not expired
* * Checks that `token` has a valid signature that matches the public key it claims
* * Checks that both the manifest and redirect URLs are absolute and conform to
* the same origin policy
*
* @param {String} token encoded and signed authentication request token
* @return {Promise} that resolves to true if the auth request
* is valid and false if it does not. It rejects with a String if the
* token is not signed
* @private
*/
function verifyAuthRequest(token) {
return new Promise(function (resolve, reject) {
if ((0, _jsontokens.decodeToken)(token).header.alg === 'none') {
reject('Token must be signed in order to be verified');
}
Promise.all([isExpirationDateValid(token), isIssuanceDateValid(token), doSignaturesMatchPublicKeys(token), doPublicKeysMatchIssuer(token), isManifestUriValid(token), isRedirectUriValid(token)]).then(function (values) {
if (values.every(Boolean)) {
resolve(true);
} else {
resolve(false);
}
});
});
}
/**
* Verify the authentication request is valid and
* fetch the app manifest file if valid. Otherwise, reject the promise.
* @param {String} token encoded and signed authentication request token
* @return {Promise} that resolves to the app manifest file in JSON format
* or rejects if the auth request or app manifest file is invalid
* @private
*/
function verifyAuthRequestAndLoadManifest(token) {
return new Promise(function (resolve, reject) {
return verifyAuthRequest(token).then(function (valid) {
if (valid) {
return (0, _index.fetchAppManifest)(token).then(function (appManifest) {
resolve(appManifest);
}).catch(function (err) {
reject(err);
});
} else {
reject();
return Promise.reject();
}
});
});
}
/**
* Verify the authentication response is valid
* @param {String} token the authentication response token
* @param {String} nameLookupURL the url use to verify owner of a username
* @return {Promise} that resolves to true if auth response
* is valid and false if it does not
* @private
*/
function verifyAuthResponse(token, nameLookupURL) {
return new Promise(function (resolve) {
Promise.all([isExpirationDateValid(token), isIssuanceDateValid(token), doSignaturesMatchPublicKeys(token), doPublicKeysMatchIssuer(token), doPublicKeysMatchUsername(token, nameLookupURL)]).then(function (values) {
if (values.every(Boolean)) {
resolve(true);
} else {
resolve(false);
}
});
});
}
},{"../index":12,"jsontokens":383}],7:[function(require,module,exports){
'use strict';
Object.defineProperty(exports, "__esModule", {
value: true
});
var _authApp = require('./authApp');
Object.defineProperty(exports, 'isUserSignedIn', {
enumerable: true,
get: function get() {
return _authApp.isUserSignedIn;
}
});
Object.defineProperty(exports, 'redirectToSignIn', {
enumerable: true,
get: function get() {
return _authApp.redirectToSignIn;
}
});
Object.defineProperty(exports, 'redirectToSignInWithAuthRequest', {
enumerable: true,
get: function get() {
return _authApp.redirectToSignInWithAuthRequest;
}
});
Object.defineProperty(exports, 'getAuthResponseToken', {
enumerable: true,
get: function get() {
return _authApp.getAuthResponseToken;
}
});
Object.defineProperty(exports, 'isSignInPending', {
enumerable: true,
get: function get() {