UNPKG

@miracl/client-js

Version:
782 lines (648 loc) 24.3 kB
import Crypto from "./crypto.js"; import Users from "./users.js"; import HTTP from "./http.js"; /** * @class * @param {Object} options * @param {string} options.server - Server address, defaults to https://api.mpin.io * @param {string} options.projectId - MIRACL Trust Project ID * @param {string} options.seed - Hex encoded random number generator seed * @param {string} options.deviceName - Name of the current device * @param {Object} options.userStorage - Storage for saving user data * @param {Object} options.oidc - Parameters for initializing an OIDC auth session * @param {string} options.oidc.client_id - OIDC client ID * @param {string} options.oidc.redirect_uri - OIDC redirect URI * @param {string} options.oidc.response_type - OIDC response type. Only 'code' is supported * @param {string} options.oidc.scope - OIDC scope. Must include 'openid' * @param {string} options.oidc.state - OIDC state * @param {bool} options.cors - Enable CORS requests if set to 'true' * @param {number} options.requestTimeout - Time before a HTTP request times out in miliseconds * @param {string} options.applicationInfo - Sets additional information that will be sent via X-MIRACL-CLIENT HTTP header */ export default function Client(options) { var self = this; if (!options) { throw new Error("Invalid configuration"); } if (!options.projectId) { throw new Error("Empty project ID"); } if (!options.userStorage) { throw new Error("Invalid user storage"); } if (!options.server) { options.server = "https://api.mpin.io"; } else { // remove trailing slash from url, if there is one options.server = options.server.replace(/\/$/, ""); } // Ensure that default PIN lenght is between 4 and 6 if (!options.defaultPinLength || options.defaultPinLength > 6 || options.defaultPinLength < 4) { options.defaultPinLength = 4; } if (!options.requestTimeout || isNaN(options.requestTimeout)) { options.requestTimeout = 4000; } if (!options.oidc) { options.oidc = {}; } // Set the client name using the current lib version and provided application info options.clientName = "MIRACL Client.js/8.7.0" + (options.applicationInfo ? " " + options.applicationInfo : ""); self.options = options; self.http = new HTTP(options.requestTimeout, options.clientName, options.projectId, options.cors); self.crypto = new Crypto(options.seed); self.users = new Users(options.userStorage, options.projectId, "mfa"); } Client.prototype.options = {}; Client.prototype.session = {}; /** * Set the access(session) ID * * @param {string} accessId */ Client.prototype.setAccessId = function (accessId) { this.session.accessId = accessId; }; /** * Make a request to start a new session and fetch the access(session) ID * * @param {string} userId - The unique identifier of the user that will be authenticating (not required) * @param {function(Error, Object)} callback */ Client.prototype.fetchAccessId = function (userId, callback) { var self = this, reqData; reqData = { url: self.options.server + "/rps/v2/session", type: "POST", data: { projectId: self.options.projectId, userId: userId } }; self.http.request(reqData, function (error, res) { if (error) { return callback(error, null); } self.session = res; callback(null, res); }); }; /** * Request for changes in status * * @param {function(Error, Object)} callback */ Client.prototype.fetchStatus = function (callback) { var self = this, reqData; reqData = { url: self.options.server + "/rps/v2/access", type: "POST", data: { webOTT: self.session.webOTT } }; self.http.request(reqData, function (error, data) { if (error) { return callback(error, null); } callback(null, data); }); }; /** * Start the push authentication flow * * @param {string} userId - The unique identifier of the user that will be authenticating * @param {function(Error, Object)} callback */ Client.prototype.sendPushNotificationForAuth = function (userId, callback) { var self = this, reqData; if (!userId) { return callback(new Error("Empty user ID"), null); } reqData = { url: self.options.server + "/pushauth?" + self._urlEncode(self.options.oidc), type: "POST", data: { prerollId: userId } }; self.http.request(reqData, function (err, result) { if (err) { if (result && result.error === "NO_PUSH_TOKEN") { return callback(new Error("No push token", { cause: err })); } return callback(err, null); } self.session.webOTT = result.webOTT; callback(null, result); }); }; /** * Start the verification process for a specified user ID (must be email) * * @param {string} userId - The email to start verification for * @param {function(Error, Object)} callback */ Client.prototype.sendVerificationEmail = function (userId, callback) { var self = this, reqData = {}; if (!userId) { return callback(new Error("Empty user ID"), null); } reqData.url = self.options.server + "/verification/email"; reqData.type = "POST"; reqData.data = { userId: userId, mpinId: self.users.get(userId, "mpinId"), projectId: self.options.projectId, accessId: self.session.accessId, deviceName: self._getDeviceName(), clientId: self.options.oidc["client_id"], redirectURI: self.options.oidc["redirect_uri"], scope: self.options.oidc["scope"] ? self.options.oidc["scope"].split(" ") : [], state: self.options.oidc["state"], nonce: self.options.oidc["nonce"] }; self.http.request(reqData, function (err, result) { if (err) { if (result && result.error === "REQUEST_BACKOFF") { return callback(new Error("Request backoff", { cause: err }), result); } return callback(new Error("Verification fail", { cause: err }), result); } callback(null, result); }); }; /** * Finish the verification process * * @param {string} verificationURI - The URI received in the email containing the verification code * @param {function(Error, Object)} callback */ Client.prototype.getActivationToken = function (verificationURI, callback) { var self = this, reqData = {}, params; params = self._parseUriParams(verificationURI); if (!params["user_id"]) { return callback(new Error("Empty user ID"), null); } if (!params["code"]) { return callback(new Error("Empty verification code"), null); } reqData.url = self.options.server + "/verification/confirmation"; reqData.type = "POST"; reqData.data = { userId: params["user_id"], code: params["code"] }; self.http.request(reqData, function (err, result) { if (err) { if (result && result.error === "UNSUCCESSFUL_VERIFICATION") { return callback(new Error("Unsuccessful verification", { cause: err }), result); } return callback(new Error("Get activation token fail", { cause: err }), result); } result.userId = params["user_id"]; callback(null, result); }); }; /** * Create an identity for the specified user ID * * @param {string} userId - The unique identifier of the user * @param {string} activationToken - The code received from the verification process * @param {function} pinCallback - Called when the PIN code needs to be entered * @param {function(Error, Object)} callback */ Client.prototype.register = function (userId, activationToken, pinCallback, callback) { var self = this, keypair; if (!userId) { return callback(new Error("Empty user ID"), null); } if (!activationToken) { return callback(new Error("Empty activation token"), null); } keypair = self.crypto.generateKeypair("BN254CX"); self._createMPinID(userId, activationToken, keypair, function (err, identityData) { if (err) { if (identityData && identityData.error === "INVALID_ACTIVATION_TOKEN") { return callback(new Error("Invalid activation token", { cause: err }), null); } return callback(new Error("Registration fail", { cause: err }), null); } if (identityData.projectId !== self.options.projectId) { return callback(new Error("Project mismatch"), null); } self._getSecret(identityData.secretUrls[0], function (err, sec1Data) { if (err) { return callback(new Error("Registration fail", { cause: err }), null); } self._getSecret(identityData.secretUrls[1], function (err, sec2Data) { if (err) { return callback(new Error("Registration fail", { cause: err }), null); } var pinLength, passPin; pinLength = identityData.pinLength; if (!pinLength) { pinLength = self.options.defaultPinLength; } // should be called to continue the flow // after a PIN was provided passPin = function (userPin) { self._createIdentity(userId, userPin, identityData, sec1Data, sec2Data, keypair, callback); }; pinCallback(passPin, pinLength); }); }); }); }; Client.prototype._createMPinID = function (userId, activationToken, keypair, callback) { var self = this, regData = {}; regData.url = self.options.server + "/registration"; regData.type = "POST"; regData.data = { userId: userId, deviceName: self._getDeviceName(), activationToken: activationToken, publicKey: keypair.publicKey }; self.http.request(regData, function (err, result) { if (err) { return callback(err, result); } self.users.write(userId, { state: self.users.states.start }); callback(null, result); }); }; Client.prototype._getDeviceName = function () { var self = this; if (self.options.deviceName) { return self.options.deviceName; } return "Browser"; }; Client.prototype._getSecret = function (secretUrl, callback) { var self = this, requestData = { url: secretUrl }; self.http.request(requestData, function (err, result) { if (err) { if (err.message === "The request was aborted") { self.http.request(requestData, callback); } else { callback(err, result); } return; } callback(null, result); }); }; Client.prototype._createIdentity = function (userId, userPin, identityData, sec1Data, sec2Data, keypair, callback) { var self = this, userData, csHex, token; try { csHex = self.crypto.addShares(keypair.privateKey, sec1Data.dvsClientSecret, sec2Data.dvsClientSecret, identityData.curve); token = self.crypto.extractPin(identityData.mpinId, keypair.publicKey, userPin, csHex, identityData.curve); } catch (err) { return callback(err, null); } userData = { mpinId: identityData.mpinId, token: token, curve: identityData.curve, dtas: identityData.dtas, publicKey: keypair.publicKey, pinLength: identityData.pinLength, projectId: identityData.projectId, verificationType: identityData.verificationType, state: self.users.states.register, nowTime: identityData.nowTime, updated: Math.floor(Date.now() / 1000) }; self.users.write(userId, userData); callback(null, userData); }; /** * Authenticate the user with the specified user ID * * @param {string} userId - The unique identifier of the user * @param {string} userPin - The PIN associated with the userId * @param {function(Error, Object)} callback */ Client.prototype.authenticate = function (userId, userPin, callback) { this._authentication(userId, userPin, ["jwt"], callback); }; /** * Authenticate the user for the session specified by the qrCode parameter * * @param {string} userId - The unique identifier of the user * @param {string} qrCode - The QR code URL that initiated the authentication * @param {string} userPin - The PIN associated with the userId * @param {function(Error, Object)} callback */ Client.prototype.authenticateWithQRCode = function (userId, qrCode, userPin, callback) { this.setAccessId(qrCode.split("#").pop()); this._authentication(userId, userPin, ["oidc"], callback); }; /** * Authenticate the user for the session specified by the appLink parameter * * @param {string} userId - The unique identifier of the user * @param {string} appLink - The app link that initiated the authentication * @param {string} userPin - The PIN associated with the userId * @param {function(Error, Object)} callback */ Client.prototype.authenticateWithAppLink = function (userId, appLink, userPin, callback) { this.setAccessId(appLink.split("#").pop()); this._authentication(userId, userPin, ["oidc"], callback); }; /** * Authenticate the session specified by the push notification payload * * @param {[key: string]: string} payload - The push notification payload * @param {string} userPin - The PIN associated with the userId * @param {function(Error, Object)} callback */ Client.prototype.authenticateWithNotificationPayload = function (payload, userPin, callback) { if (!payload || !payload["userID"] || !payload["qrURL"]) { return callback(new Error("Invalid push notification payload"), null); } this.setAccessId(payload["qrURL"].split("#").pop()); this._authentication(payload["userID"], userPin, ["oidc"], callback); }; /** * Fetch a registration (bootstrap) code for the specified user ID * * @param {string} userId - The unique identifier of the user * @param {string} userPin - The PIN associated with the userId * @param {function(Error, Object)} callback */ Client.prototype.generateQuickCode = function (userId, userPin, callback) { var self = this; self._authentication(userId, userPin, ["reg-code"], function (err, result) { if (err) { return callback(err, null); } self.http.request({ url: self.options.server + "/verification/quickcode", type: "POST", data: { projectId: self.options.projectId, jwt: result.jwt, deviceName: self._getDeviceName() } }, function (err, result) { if (err) { return callback(err, null); } callback(null, { code: result.code, expireTime: result.expireTime, ttlSeconds: result.ttlSeconds, // Deprecated, kept for backward compatibility OTP: result.code }); }); }); }; Client.prototype._authentication = function (userId, userPin, scope, callback) { var self = this, identityData, SEC = [], X = []; if (!userId) { return callback(new Error("Empty user ID"), null); } if (!self.users.exists(userId)) { return callback(new Error("User not found"), null); } identityData = self.users.get(userId); self._getPass1(identityData, userPin, scope, X, SEC, function (err, pass1Data) { if (err) { if (pass1Data && pass1Data.error === "EXPIRED_MPINID") { self.users.write(userId, { state: self.users.states.revoked }); return callback(new Error("Revoked", { cause: err }), null); } return callback(new Error("Authentication fail", { cause: err }), null); } self._getPass2(identityData, scope, pass1Data.y, X, SEC, function (err, pass2Data) { if (err) { return callback(new Error("Authentication fail", { cause: err }), null); } self._finishAuthentication(userId, userPin, scope, pass2Data.authOTT, function (err, result) { if (err) { if (result && result.error === "UNSUCCESSFUL_AUTHENTICATION") { return callback(new Error("Unsuccessful authentication", { cause: err }), null); } if (result && result.error === "REVOKED_MPINID") { self.users.write(userId, { state: self.users.states.revoked }); return callback(new Error("Revoked", { cause: err }), null); } return callback(new Error("Authentication fail", { cause: err }), null); } callback(null, result); }); }); }); }; /** * Make a request for pass one of the M-Pin protocol * * This function assigns to the property X a random value. It assigns to * the property SEC the sum of the client secret and time permit. It also * calculates the values U and UT which are required for M-Pin authentication, * where U = X.(map_to_curve(MPIN_ID)) and UT = X.(map_to_curve(MPIN_ID) + map_to_curve(DATE|sha256(MPIN_ID)) * UT is called the commitment. U is the required for finding the PIN error. * * Request data has the following structure: * { * mpin_id: mpinIdHex, // Hex encoded M-Pin ID * dtas: dtaList // Identifier of the DTAs used for this identity * UT: UT_hex, // Hex encoded UT * U: U_hex, // Hex encoded U * publicKey: publicKey, // The public key used for DVS * scope: ['oidc'] // Scope of the authentication * } * @private */ Client.prototype._getPass1 = function (identityData, userPin, scope, X, SEC, callback) { var self = this, res, requestData; try { res = self.crypto.calculatePass1(identityData.curve, identityData.mpinId, identityData.publicKey, identityData.token, userPin, X, SEC); } catch (err) { return callback(err, null); } requestData = { scope: scope, mpin_id: identityData.mpinId, dtas: identityData.dtas, publicKey: identityData.publicKey, UT: res.UT, U: res.U }; self.http.request({ url: self.options.server + "/rps/v2/pass1", type: "POST", data: requestData }, callback); }; /** * Make a request for pass two of the M-Pin protocol * * This function uses the random value y from the server, property X * and the combined client secret and time permit to calculate * the value V which is sent to the M-Pin server. * * Request data has the following structure: * { * mpin_id: mpinIdHex, // Hex encoded M-Pin ID * V: V_hex, // Value required by the server to authenticate user * WID: accessNumber // Number required for mobile authentication * } * @private */ Client.prototype._getPass2 = function (identityData, scope, yHex, X, SEC, callback) { var self = this, vHex, requestData; try { vHex = self.crypto.calculatePass2(identityData.curve, X, yHex, SEC); } catch (err) { return callback(err, null); } requestData = { mpin_id: identityData.mpinId, WID: self.session.accessId, V: vHex }; self.http.request({ url: self.options.server + "/rps/v2/pass2", type: "POST", data: requestData}, callback); }; Client.prototype._finishAuthentication = function (userId, userPin, scope, authOTT, callback) { var self = this, requestData; requestData = { "authOTT": authOTT, "wam": "dvs" }; self.http.request({ url: self.options.server + "/rps/v2/authenticate", type: "POST", data: requestData }, function (err, result) { if (err) { return callback(err, result); } if (result.dvsRegister) { self._renewSecret(userId, userPin, result.dvsRegister, function(err) { if (err) { return callback(err, null); } self._authentication(userId, userPin, scope, callback); }); } else { self.users.updateLastUsed(userId); callback(null, result); } }); }; Client.prototype._renewSecret = function (userId, userPin, activationData, callback) { var self = this, keypair; keypair = self.crypto.generateKeypair(activationData.curve); self._createMPinID(userId, activationData.token, keypair, function (err, identityData) { if (err) { return callback(err, null); } self._getSecret(identityData.secretUrls[0], function (err, sec1Data) { if (err) { return callback(err, null); } self._getSecret(identityData.secretUrls[1], function (err, sec2Data) { if (err) { return callback(err, null); } self._createIdentity(userId, userPin, identityData, sec1Data, sec2Data, keypair, callback); }); }); }); }; /** * Create a cryptographic signature of a given message * * @param {string} userId - The unique identifier of the user * @param {string} userPin - The PIN associated with the userId * @param {string} message - The message that will be signed * @param {number} timestamp - The creation timestamp of the message * @param {function(Error, Object)} callback */ Client.prototype.sign = function (userId, userPin, message, timestamp, callback) { var self = this, identityData; if (!userId) { return callback(new Error("Empty user ID"), null); } if (!self.users.exists(userId)) { return callback(new Error("User not found"), null); } if (!message) { return callback(new Error("Empty message"), null); } identityData = self.users.get(userId); if (!identityData.publicKey) { return callback(new Error("Empty public key"), null); } this._authentication(userId, userPin, ["dvs-auth"], function (err) { var res, signatureData; if (err) { switch (err.message) { case "Unsuccessful authentication": case "Revoked": return callback(err, null); default: return callback(new Error("Signing fail", { cause: err.cause }), null); } } try { res = self.crypto.sign(identityData.curve, identityData.mpinId, identityData.publicKey, identityData.token, userPin, message, timestamp); } catch (err) { return callback(new Error("Signing fail", { cause: err }), null); } signatureData = { hash: message, u: res.U, v: res.V, mpinId: identityData.mpinId, publicKey: identityData.publicKey, dtas: identityData.dtas }; callback(null, signatureData); }); }; Client.prototype._urlEncode = function (obj) { var str = [], p; for (p in obj) { if (Object.prototype.hasOwnProperty.call(obj, p)) { str.push(encodeURIComponent(p) + "=" + encodeURIComponent(obj[p])); } } return str.join("&"); }; Client.prototype._parseUriParams = function (uri) { var query = uri.split("?").pop(), queryArr = query.split("&"), params = {}, pairArr, i; if (!query.length || !queryArr.length) { return params; } for (i = 0; i < queryArr.length; i++) { pairArr = queryArr[i].split("="); params[pairArr[0]] = decodeURIComponent(pairArr[1].replace(/\+/g, " ")); } return params; };