UNPKG

@authsignal/node

Version:

[![NPM version](https://img.shields.io/npm/v/@authsignal/node.svg)](https://npmjs.org/package/@authsignal/node) [![License](https://img.shields.io/npm/l/@authsignal/node.svg)](https://github.com/authsignal/authsignal-node/blob/main/LICENSE.md)

471 lines (460 loc) 18.1 kB
'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); var axios = require('axios'); var axiosRetry = require('axios-retry'); var crypto = require('crypto'); function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; } var axios__default = /*#__PURE__*/_interopDefaultLegacy(axios); var axiosRetry__default = /*#__PURE__*/_interopDefaultLegacy(axiosRetry); class AuthsignalError extends Error { constructor(statusCode, errorCode, errorDescription, axiosError) { const message = formatMessage(statusCode, errorCode, errorDescription); super(message); this.name = "AuthsignalError"; this.statusCode = statusCode; this.errorCode = errorCode; this.errorDescription = errorDescription; this.axiosError = axiosError; } } function mapToAuthsignalError(error) { if (error instanceof axios.AxiosError) { const { response } = error; if (response === null || response === void 0 ? void 0 : response.data) { const { error: errorCode, errorDescription } = response.data; return new AuthsignalError(response.status, errorCode, errorDescription, error); } } if (error instanceof Error) { return new AuthsignalError(500, "unexpected_error", error.message); } return new AuthsignalError(500, "unexpected_error"); } function formatMessage(statusCode, errorCode, errorDescription) { return `AuthsignalError: ${statusCode} - ${formatDescription(errorCode, errorDescription)}`; } function formatDescription(errorCode, errorDescription) { return errorDescription && errorDescription.length > 0 ? errorDescription : errorCode; } // Default tolerance (in minutes) for difference between timestamp in signature and current time // This is used to prevent replay attacks const DEFAULT_TOLERANCE = 5; class Webhook { constructor(apiSecretKey) { this.apiSecretKey = apiSecretKey; } constructEvent(payload, signature, tolerance = DEFAULT_TOLERANCE) { const parsedSignature = this.parseSignature(signature); const secondsSinceEpoch = Math.round(Date.now() / 1000); if (tolerance > 0 && parsedSignature.timestamp < secondsSinceEpoch - tolerance * 60) { throw new InvalidSignatureError("Timestamp is outside the tolerance zone."); } const hmacContent = parsedSignature.timestamp + "." + payload; const computedSignature = crypto.createHmac("sha256", this.apiSecretKey) .update(hmacContent) .digest("base64") .replace("=", ""); let match = false; for (const signature of parsedSignature.signatures) { if (signature === computedSignature) { match = true; } } if (!match) { throw new InvalidSignatureError("Signature mismatch."); } return JSON.parse(payload); } parseSignature(value) { const parsedValue = value === null || value === void 0 ? void 0 : value.split(",").reduce((acc, item) => { const kv = item.split("="); if (kv[0] === "t") { acc.timestamp = parseInt(kv[1], 10); } if (kv[0] === VERSION$1) { acc.signatures.push(kv[1]); } return acc; }, { timestamp: -1, signatures: [], }); if (!parsedValue || parsedValue.timestamp === -1 || parsedValue.signatures.length === 0) { throw new InvalidSignatureError("Signature format is invalid."); } return parsedValue; } } const VERSION$1 = "v2"; class InvalidSignatureError extends Error { constructor(message) { super(message); } } const DEFAULT_API_URL = "https://api.authsignal.com/v1"; const VERSION = "2.6.0"; const DEFAULT_RETRIES = 2; const RETRY_ERROR_CODES = ["ECONNRESET", "EPIPE", "ECONNREFUSED"]; const SAFE_HTTP_METHODS = ["GET", "HEAD", "OPTIONS"]; function isRetryableAuthsignalError(error) { // Retry on connection error if (!error.response) { return true; } if (error.code && RETRY_ERROR_CODES.includes(error.code)) { return true; } const { method } = error.request; const { status } = error.response; if (status >= 500 && status <= 599) { if (method && SAFE_HTTP_METHODS.includes(method)) { return true; } } return false; } class Authsignal { constructor({ apiSecretKey, apiUrl, retries }) { this.apiSecretKey = apiSecretKey; this.apiUrl = apiUrl !== null && apiUrl !== void 0 ? apiUrl : DEFAULT_API_URL; const axiosRetries = retries !== null && retries !== void 0 ? retries : DEFAULT_RETRIES; if (axiosRetries > 0) { axiosRetry__default["default"](axios__default["default"], { retries: axiosRetries, retryDelay: axiosRetry__default["default"].exponentialDelay, retryCondition: isRetryableAuthsignalError, }); } this.webhook = new Webhook(apiSecretKey); } async getUser(request) { const { userId } = request; const url = `${this.apiUrl}/users/${userId}`; const config = this.getRequestConfig(); try { const response = await axios__default["default"].get(url, config); return response.data; } catch (error) { throw mapToAuthsignalError(error); } } async queryUsers(request) { const { username, email, phoneNumber, token, limit, lastEvaluatedUserId } = request; const url = new URL(`${this.apiUrl}/users`); if (username) { url.searchParams.set("username", username); } if (email) { url.searchParams.set("email", email); } if (phoneNumber) { url.searchParams.set("phoneNumber", phoneNumber); } if (token) { url.searchParams.set("token", token); } if (limit) { url.searchParams.set("limit", limit.toString()); } if (lastEvaluatedUserId) { url.searchParams.set("lastEvaluatedUserId", lastEvaluatedUserId); } const config = this.getRequestConfig(); try { const response = await axios__default["default"].get(url.toString(), config); return response.data; } catch (error) { throw mapToAuthsignalError(error); } } async updateUser(request) { const { userId, attributes } = request; const url = `${this.apiUrl}/users/${userId}`; const config = this.getRequestConfig(); try { const response = await axios__default["default"].patch(url, attributes, config); return response.data; } catch (error) { throw mapToAuthsignalError(error); } } async deleteUser(request) { const { userId } = request; const url = `${this.apiUrl}/users/${userId}`; const config = this.getRequestConfig(); try { await axios__default["default"].delete(url, config); } catch (error) { throw mapToAuthsignalError(error); } } async getAuthenticators(request) { const { userId } = request; const url = `${this.apiUrl}/users/${userId}/authenticators`; const config = this.getRequestConfig(); try { const response = await axios__default["default"].get(url, config); return response.data; } catch (error) { throw mapToAuthsignalError(error); } } async enrollVerifiedAuthenticator(request) { const { userId, attributes } = request; const url = `${this.apiUrl}/users/${userId}/authenticators`; const config = this.getRequestConfig(); try { const response = await axios__default["default"].post(url, attributes, config); return response.data; } catch (error) { throw mapToAuthsignalError(error); } } async deleteAuthenticator(request) { const { userId, userAuthenticatorId } = request; const url = `${this.apiUrl}/users/${userId}/authenticators/${userAuthenticatorId}`; const config = this.getRequestConfig(); try { await axios__default["default"].delete(url, config); } catch (error) { throw mapToAuthsignalError(error); } } async track(request) { const { userId, action, attributes = {} } = request; const url = `${this.apiUrl}/users/${userId}/actions/${action}`; const config = this.getRequestConfig(); try { const response = await axios__default["default"].post(url, attributes, config); return response.data; } catch (error) { throw mapToAuthsignalError(error); } } async validateChallenge(request) { const url = `${this.apiUrl}/validate`; const config = this.getRequestConfig(); try { const response = await axios__default["default"].post(url, request, config); const { actionCode: action, ...rest } = response.data; return { action, ...rest }; } catch (error) { throw mapToAuthsignalError(error); } } async claimChallenge(request) { const url = `${this.apiUrl}/claim`; const config = this.getRequestConfig(); try { const response = await axios__default["default"].post(url, request, config); return response.data; } catch (error) { throw mapToAuthsignalError(error); } } async getAction(request) { const { userId, action, idempotencyKey } = request; const url = `${this.apiUrl}/users/${userId}/actions/${action}/${idempotencyKey}`; const config = this.getRequestConfig(); try { const response = await axios__default["default"].get(url, config); return response.data; } catch (error) { throw mapToAuthsignalError(error); } } async queryUserActions(request) { const { userId, fromDate, actionCodes = [], state } = request; const url = new URL(`${this.apiUrl}/users/${userId}/actions`); if (fromDate) { url.searchParams.set("fromDate", fromDate); } if (actionCodes.length > 0) { url.searchParams.set("codes", actionCodes.join(",")); } if (state) { url.searchParams.set("state", state); } const config = this.getRequestConfig(); try { const response = await axios__default["default"].get(url.toString(), config); return response.data; } catch (error) { throw mapToAuthsignalError(error); } } async updateAction(request) { const { userId, action, idempotencyKey, attributes } = request; const url = `${this.apiUrl}/users/${userId}/actions/${action}/${idempotencyKey}`; const config = this.getRequestConfig(); try { const response = await axios__default["default"].patch(url, attributes, config); return response.data; } catch (error) { throw mapToAuthsignalError(error); } } async challenge(request) { const url = `${this.apiUrl}/challenge`; const config = this.getRequestConfig(); try { const response = await axios__default["default"].post(url, request, config); return response.data; } catch (error) { throw mapToAuthsignalError(error); } } async verify(request) { const url = `${this.apiUrl}/verify`; const config = this.getRequestConfig(); try { const response = await axios__default["default"].post(url, request, config); return response.data; } catch (error) { throw mapToAuthsignalError(error); } } async getChallenge(request) { const { challengeId, userId, action, verificationMethod } = request; const url = new URL(`${this.apiUrl}/challenges`); if (challengeId) { url.searchParams.set("challengeId", challengeId); } if (userId) { url.searchParams.set("userId", userId); } if (action) { url.searchParams.set("action", action); } if (verificationMethod) { url.searchParams.set("verificationMethod", verificationMethod); } const config = this.getRequestConfig(); try { const response = await axios__default["default"].get(url.toString(), config); return response.data; } catch (error) { throw mapToAuthsignalError(error); } } getRequestConfig() { return { auth: { username: this.apiSecretKey, password: "", }, headers: { "X-Authsignal-Version": VERSION, "User-Agent": "authsignal-node", }, }; } } exports.UserActionState = void 0; (function (UserActionState) { UserActionState["ALLOW"] = "ALLOW"; UserActionState["BLOCK"] = "BLOCK"; UserActionState["CHALLENGE_REQUIRED"] = "CHALLENGE_REQUIRED"; UserActionState["CHALLENGE_SUCCEEDED"] = "CHALLENGE_SUCCEEDED"; UserActionState["CHALLENGE_FAILED"] = "CHALLENGE_FAILED"; UserActionState["REVIEW_REQUIRED"] = "REVIEW_REQUIRED"; UserActionState["REVIEW_SUCCEEDED"] = "REVIEW_SUCCEEDED"; UserActionState["REVIEW_FAILED"] = "REVIEW_FAILED"; })(exports.UserActionState || (exports.UserActionState = {})); exports.VerificationMethod = void 0; (function (VerificationMethod) { VerificationMethod["SMS"] = "SMS"; VerificationMethod["EMAIL_OTP"] = "EMAIL_OTP"; VerificationMethod["EMAIL_MAGIC_LINK"] = "EMAIL_MAGIC_LINK"; VerificationMethod["AUTHENTICATOR_APP"] = "AUTHENTICATOR_APP"; VerificationMethod["PASSKEY"] = "PASSKEY"; VerificationMethod["SECURITY_KEY"] = "SECURITY_KEY"; VerificationMethod["PUSH"] = "PUSH"; VerificationMethod["VERIFF"] = "VERIFF"; VerificationMethod["IPROOV"] = "IPROOV"; VerificationMethod["IDVERSE"] = "IDVERSE"; VerificationMethod["PALM_BIOMETRICS_RR"] = "PALM_BIOMETRICS_RR"; VerificationMethod["RECOVERY_CODE"] = "RECOVERY_CODE"; })(exports.VerificationMethod || (exports.VerificationMethod = {})); exports.SmsChannel = void 0; (function (SmsChannel) { SmsChannel["WHATSAPP"] = "WHATSAPP"; SmsChannel["DEFAULT"] = "DEFAULT"; })(exports.SmsChannel || (exports.SmsChannel = {})); const DEFAULT_ACTION_NAME = "auth0-login"; // eslint-disable-next-line @typescript-eslint/no-explicit-any async function handleAuth0ExecutePostLogin(event, api, options) { var _a, _b, _c; // Redirects are not possible for refresh token exchange // https://auth0.com/docs/customize/actions/flows-and-triggers/login-flow/redirect-with-actions#refresh-tokens if (((_a = event.transaction) === null || _a === void 0 ? void 0 : _a.protocol) === "oauth2-refresh-token") { return; } const { apiSecretKey = event.secrets.AUTHSIGNAL_SECRET, userId = event.user.user_id, action = DEFAULT_ACTION_NAME, redirectUrl = `https://${event.request.hostname}/continue`, custom = {}, apiUrl = DEFAULT_API_URL, forceEnrollment = false, } = options !== null && options !== void 0 ? options : {}; const sessionMfaMethod = (_b = event.authentication) === null || _b === void 0 ? void 0 : _b.methods.find(({ name }) => name === apiUrl); // If user has already completed MFA for the current Auth0 session, don't prompt again if (sessionMfaMethod) { return; } const authsignal = new Authsignal({ apiSecretKey, apiUrl }); const result = await authsignal.track({ action, userId, attributes: { redirectUrl, custom, email: event.user.email, ipAddress: event.request.ip, userAgent: event.request.user_agent, deviceId: (_c = event.request.query) === null || _c === void 0 ? void 0 : _c["device_id"], }, }); const { isEnrolled, state, url } = result; const challengeUrl = forceEnrollment ? `${url}&force_enrollment=true` : url; if (!isEnrolled || state === exports.UserActionState.CHALLENGE_REQUIRED) { api.redirect.sendUserTo(challengeUrl); } else if (state === exports.UserActionState.BLOCK) { api.access.deny("Action blocked"); } } // eslint-disable-next-line @typescript-eslint/no-explicit-any async function handleAuth0ContinuePostLogin(event, api, options) { var _a; const { apiSecretKey = event.secrets.AUTHSIGNAL_SECRET, userId = event.user.user_id, action = DEFAULT_ACTION_NAME, failureMessage = "MFA challenge failed", apiUrl = DEFAULT_API_URL, } = options !== null && options !== void 0 ? options : {}; const authsignal = new Authsignal({ apiSecretKey, apiUrl }); const result = await authsignal.validateChallenge({ token: (_a = event.request.query) === null || _a === void 0 ? void 0 : _a["token"], action, userId, }); if (result.action !== action || result.state !== exports.UserActionState.CHALLENGE_SUCCEEDED) { api.access.deny(failureMessage); } else { api.authentication.recordMethod(apiUrl); } } exports.Authsignal = Authsignal; exports.AuthsignalError = AuthsignalError; exports.DEFAULT_API_URL = DEFAULT_API_URL; exports.Webhook = Webhook; exports.handleAuth0ContinuePostLogin = handleAuth0ContinuePostLogin; exports.handleAuth0ExecutePostLogin = handleAuth0ExecutePostLogin; exports.mapToAuthsignalError = mapToAuthsignalError;