@authsignal/node
Version:
[](https://npmjs.org/package/@authsignal/node) [](https://github.com/authsignal/authsignal-node/blob/main/LICENSE.md)
471 lines (460 loc) • 18.1 kB
JavaScript
;
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;