Skip to main content

Initial setup

Overview

The following page shows you how to include the Attack Protection Suite feature in your SuperTokens integration.

Before you start

This feature is in beta. To get access to it, please reach out to get it set up for you. Once you have access to it, you will receive:

  • Public API key - use this on your frontend for generating request IDs
  • Secret API key - use this on your backend for making requests to the anomaly detection API
  • Environment ID - use this for identifying the environment you are using both on the backend and the frontend

You can use the feature with either the Email Password or the Passwordless authentication methods. For social or enterprise login, it is not needed for several reasons:

  • Existing anomaly detection: Most reputable third-party authentication providers (like Google, Facebook, Apple, etc.) have robust security measures in place, including their own anomaly detection systems. These systems are typically more comprehensive and tailored to their specific platforms.
  • Limited visibility: When using third-party authentication, you have limited visibility into the authentication process. This makes it difficult to accurately detect anomalies or suspicious activities that occur on the third-party's side.
  • Potential false positives: Applying anomaly detection to third-party logins might lead to an increase in false positives, as you don't have full context of the user's interactions with the third-party provider.
  • User experience: Additional security checks on top of third-party authentication could negatively impact the user experience, defeating the purpose of offering third-party login as a convenient option.

Steps

1. Attach request IDs to backend API calls

The Attack Protection Suite feature relies on identifying each request through a unique ID. This way the fingerprinting process can determine if it's a potential threat or not.

Important

This step applies only to bot detection and anomaly IP-based detection such as impossible travel detection. Also, check for bot detection only on the email password login flows.

1.1 Generate a request ID

To generate a request ID, import, and initialize the SDK using your public API key. This SDK generates a unique request ID for each authentication event attempt.

const ENVIRONMENT_ID = "<environment-id>"; // Your environment ID that you received from the SuperTokens team
// Initialize the agent on page load using your public API key that you received from the SuperTokens team.
const supertokensRequestIdPromise = require("https://deviceid.supertokens.io/PqWNQ35Ydhm6WDUK/k9bwGCuvuA83Ad6s?apiKey=<PUBLIC_API_KEY>")
.then((RequestId: any) => RequestId.load({
endpoint: [
'https://deviceid.supertokens.io/PqWNQ35Ydhm6WDUK/CnsdzKsyFKU8Q3h2',
RequestId.defaultEndpoint
]
}));

async function getRequestId() {
const sdk = await supertokensRequestIdPromise;
const result = await sdk.get({
tag: {
environmentId: ENVIRONMENT_ID,
}
});
return result.requestId;
}

1.2 Pass the request ID to the backend

Include the requestId property along with the value as part of the preAPIHook body from the initialisation of the recipes.

Important

If the request ID is not passed to the backend, the anomaly detection can only detect password breaches and brute force attacks.

Below is a full example of how to configure the SDK and pass the request ID to the backend. The request ID generates only for the email password sign in, sign up, and reset password actions because these are the only actions that require bot detection. For all the other recipes, this is not needed.

import EmailPassword from "supertokens-auth-react/recipe/emailpassword";

const ENVIRONMENT_ID = "<environment-id>"; // Your environment ID that you received from the SuperTokens team
// Initialize the agent on page load using your public API key that you received from the SuperTokens team.
const supertokensRequestIdPromise = require("https://deviceid.supertokens.io/PqWNQ35Ydhm6WDUK/k9bwGCuvuA83Ad6s?apiKey=<PUBLIC_API_KEY>")
.then((RequestId: any) => RequestId.load({
endpoint: [
'https://deviceid.supertokens.io/PqWNQ35Ydhm6WDUK/CnsdzKsyFKU8Q3h2',
RequestId.defaultEndpoint
]
}));

async function getRequestId() {
const sdk = await supertokensRequestIdPromise;
const result = await sdk.get({
tag: {
environmentId: ENVIRONMENT_ID,
}
});
return result.requestId;
}

export const SuperTokensConfig = {
// ... other config options
appInfo: {
appName: "...",
apiDomain: '...',
websiteDomain: '...',
},
// recipeList contains all the modules that you want to
// use from SuperTokens. See the full list here: https://supertokens.com/docs/guides
recipeList: [
EmailPassword.init({
preAPIHook: async (context) => {
let url = context.url;
let requestInit = context.requestInit;

let action = context.action;
if (action === "EMAIL_PASSWORD_SIGN_IN" || action === "EMAIL_PASSWORD_SIGN_UP" || action === "SEND_RESET_PASSWORD_EMAIL") {
let requestId = await getRequestId();
let body = context.requestInit.body;
if (body !== undefined) {
let bodyJson = JSON.parse(body as string);
bodyJson.requestId = requestId;
requestInit.body = JSON.stringify(bodyJson);
}
}
return {
requestInit, url
};
}
}),
],
};

2. Retrieve the request ID

To retrieve the request ID in the backend you have to override the recipe implementations.

Email and Password

import SuperTokens from "supertokens-node";
import EmailPassword from "supertokens-node/recipe/emailpassword";
import Session from "supertokens-node/recipe/session";
import axios from "axios";
import { createHash } from "crypto";

function getIpFromRequest(req: Request): string {
let headers: { [key: string]: string } = {};
for (let key of Object.keys(req.headers)) {
headers[key] = (req as any).headers[key]!;
}
return (req as any).headers["x-forwarded-for"] || "127.0.0.1";
}

const getBruteForceConfig = (
userIdentifier: string,
ip: string,
prefix?: string,
) => [
{
key: `${prefix ? `${prefix}-` : ""}${userIdentifier}`,
maxRequests: [
{ limit: 5, perTimeIntervalMS: 60 * 1000 },
{ limit: 15, perTimeIntervalMS: 60 * 60 * 1000 },
],
},
{
key: `${prefix ? `${prefix}-` : ""}${ip}`,
maxRequests: [
{ limit: 5, perTimeIntervalMS: 60 * 1000 },
{ limit: 15, perTimeIntervalMS: 60 * 60 * 1000 },
],
},
];


SuperTokens.init({
appInfo: {
apiDomain: "...",
appName: "...",
websiteDomain: "...",
},
supertokens: {
connectionURI: "...",
},
recipeList: [
EmailPassword.init({
override: {
apis: (originalImplementation) => {
return {
...originalImplementation,
signUpPOST: async function (input) {
// We need to generate a request ID in order to detect possible bots, suspicious IP addresses, etc.
const requestId = (await input.options.req.getJSONBody())
.requestId;
if (!requestId) {
return {
status: "GENERAL_ERROR",
message: "The request ID is required",
};
}

const actionType = "emailpassword-sign-up";
const ip = getIpFromRequest(input.options.req.original);
let email = input.formFields.filter((f) => f.id === "email")[0]
.value as string;
let password = input.formFields.filter(
(f) => f.id === "password",
)[0].value as string;
const bruteForceConfig = getBruteForceConfig(
email,
ip,
actionType,
);

return originalImplementation.signUpPOST!(input);
},
signInPOST: async function (input) {
// We need to generate a request ID in order to detect possible bots, suspicious IP addresses, etc.
const requestId = (await input.options.req.getJSONBody())
.requestId;
if (!requestId) {
return {
status: "GENERAL_ERROR",
message: "The request ID is required",
};
}

const actionType = "emailpassword-sign-up";
const ip = getIpFromRequest(input.options.req.original);
let email = input.formFields.filter((f) => f.id === "email")[0]
.value as string;
let password = input.formFields.filter(
(f) => f.id === "password",
)[0].value as string;
const bruteForceConfig = getBruteForceConfig(
email,
ip,
actionType,
);

return originalImplementation.signInPOST!(input);
},
generatePasswordResetTokenPOST: async function (input) {
// We need to generate a request ID in order to detect possible bots, suspicious IP addresses, etc.
const requestId = (await input.options.req.getJSONBody())
.requestId;
if (!requestId) {
return {
status: "GENERAL_ERROR",
message: "The request ID is required",
};
}

const actionType = "emailpassword-sign-up";
const ip = getIpFromRequest(input.options.req.original);
let email = input.formFields.filter((f) => f.id === "email")[0]
.value as string;
let password = input.formFields.filter(
(f) => f.id === "password",
)[0].value as string;
const bruteForceConfig = getBruteForceConfig(
email,
ip,
actionType,
);

return originalImplementation.generatePasswordResetTokenPOST!(
input,
);
},
};
},
},
}),
],
});

Passwordless

import SuperTokens from "supertokens-node";
import Passwordless from "supertokens-node/recipe/passwordless";
import axios from "axios";

function getIpFromRequest(req: Request): string {
let headers: { [key: string]: string } = {};
for (let key of Object.keys(req.headers)) {
headers[key] = (req as any).headers[key]!;
}
return (req as any).headers["x-forwarded-for"] || "127.0.0.1";
}

const getBruteForceConfig = ( userIdentifier: string,
ip: string,
prefix?: string,
) => [
{
key: `${prefix ? `${prefix}-` : ""}${userIdentifier}`,
maxRequests: [
{ limit: 5, perTimeIntervalMS: 60 * 1000 },
{ limit: 15, perTimeIntervalMS: 60 * 60 * 1000 },
],
},
{
key: `${prefix ? `${prefix}-` : ""}${ip}`,
maxRequests: [
{ limit: 5, perTimeIntervalMS: 60 * 1000 },
{ limit: 15, perTimeIntervalMS: 60 * 60 * 1000 },
],
},
];

SuperTokens.init({
framework: "...",
appInfo: {
/*...*/
},
recipeList: [
Passwordless.init({
// ... other customisations ...
contactMethod: "EMAIL_OR_PHONE",
flowType: "USER_INPUT_CODE_AND_MAGIC_LINK",
override: {
apis: (originalImplementation) => {
return {
...originalImplementation,
createCodePOST: async function (input) {
const actionType = "passwordless-send-sms";
const ip = getIpFromRequest(input.options.req.original);
const emailOrPhoneNumber =
"email" in input ? input.email : input.phoneNumber;
const bruteForceConfig = getBruteForceConfig(
emailOrPhoneNumber,
ip,
actionType,
);

return originalImplementation.createCodePOST!(input);
},
resendCodePOST: async function (input) {
const actionType = "passwordless-send-sms";
const ip = getIpFromRequest(input.options.req.original);
let codesInfo = await Passwordless.listCodesByPreAuthSessionId({
tenantId: input.tenantId,
preAuthSessionId: input.preAuthSessionId,
});
const phoneNumber =
codesInfo && "phoneNumber" in codesInfo
? codesInfo.phoneNumber
: undefined;
const email =
codesInfo && "email" in codesInfo ? codesInfo.email : undefined;
const userIdentifier = email || phoneNumber || input.deviceId;

const bruteForceConfig = getBruteForceConfig(
userIdentifier,
ip,
actionType,
);

return originalImplementation.resendCodePOST!(input);
},
};
},
},
}),
],
});

3. Call the protection service

To use the service, send requests to the appropriate regional endpoint based on your location:

  • US Region (N. Virginia): https://security-us-east-1.aws.supertokens.io/v1/security
  • EU Region (Ireland): https://security-eu-west-1.aws.supertokens.io/v1/security
  • APAC Region (Singapore): https://security-ap-southeast-1.aws.SuperTokens.io/v1/security

Attack Protection Suite

POST /v1/security
Curl Example
curl --location --request POST 'https://security-us-east-1.aws.supertokens.io/v1/security' \
--header 'Authorization: Bearer <secret-api-key>' \
--header 'Content-Type: application/json' \
--data-raw '{
"email": "[email protected]",
"phoneNumber": "+1234567890",
"passwordHash": "9cf95dacd226dcf43da376cdb6cbba7035218920",
"requestId": "some-request-id",
"actionType": "emailpassword-sign-in",
"bruteForce": [
{
"key": "some-key",
"maxRequests": [
{
"limit": 1,
"perTimeIntervalMS": 1000
}
]
}
]
}'

Examples

Use the following examples for complete code references on how to integrate the feature.

Email and password

import SuperTokens from "supertokens-node";
import EmailPassword from "supertokens-node/recipe/emailpassword";
import Session from "supertokens-node/recipe/session";
import axios from "axios";
import { createHash } from "crypto";

const SECRET_API_KEY = "<secret-api-key>"; // Your secret API key that you received from the SuperTokens team
// The full URL with the correct region will be provided by the SuperTokens team
const ANOMALY_DETECTION_API_URL =
"https://security-<region>.aws.supertokens.io/v1/security";

async function handleSecurityChecks(input: {
actionType?: string;
email?: string;
phoneNumber?: string;
password?: string;
requestId?: string;
bruteForceConfig?: {
key: string;
maxRequests: {
limit: number;
perTimeIntervalMS: number;
}[];
}[];
}): Promise<
| {
status: "GENERAL_ERROR";
message: string;
}
| undefined
> {
let requestBody: {
email?: string;
phoneNumber?: string;
actionType?: string;
requestId?: string;
passwordHashPrefix?: string;
bruteForce?: {
key: string;
maxRequests: {
limit: number;
perTimeIntervalMS: number;
}[];
}[];
} = {};

if (input.requestId !== undefined) {
requestBody.requestId = input.requestId;
}

let passwordHash: string | undefined;
if (input.password !== undefined) {
let shasum = createHash("sha1");
shasum.update(input.password);
passwordHash = shasum.digest("hex");
requestBody.passwordHashPrefix = passwordHash.slice(0, 5);
}
requestBody.bruteForce = input.bruteForceConfig;
requestBody.email = input.email;
requestBody.phoneNumber = input.phoneNumber;
requestBody.actionType = input.actionType;

let response;
try {
response = await axios.post(ANOMALY_DETECTION_API_URL, requestBody, {
headers: {
Authorization: `Bearer ${SECRET_API_KEY}`,
"Content-Type": "application/json",
},
});
} catch (err) {
// silently fail in order to not break the auth flow
console.error(err);
return;
}

let responseData = response.data;

if (responseData.bruteForce.detected) {
return {
status: "GENERAL_ERROR",
message: "Too many requests. Please try again later.",
};
}

if (responseData.requestIdInfo?.isUsingTor) {
return {
status: "GENERAL_ERROR",
message: "Tor activity detected. Please use a regular browser.",
};
}

if (responseData.requestIdInfo?.vpn?.result) {
return {
status: "GENERAL_ERROR",
message: "VPN activity detected. Please use a regular network.",
};
}

if (responseData.requestIdInfo?.botDetected) {
return {
status: "GENERAL_ERROR",
message: "Bot activity detected.",
};
}

if (responseData?.passwordBreaches && passwordHash) {
const suffix = passwordHash.slice(5).toUpperCase();
const foundPasswordHash = responseData?.passwordBreaches[suffix];

if (foundPasswordHash) {
return {
status: "GENERAL_ERROR",
message:
"This password has been detected in a breach. Please set a different password.",
};
}
}

return undefined;
}

function getIpFromRequest(req: Request): string {
let headers: { [key: string]: string } = {};
for (let key of Object.keys(req.headers)) {
headers[key] = (req as any).headers[key]!;
}
return (req as any).headers["x-forwarded-for"] || "127.0.0.1";
}

const getBruteForceConfig = (
userIdentifier: string,
ip: string,
prefix?: string,
) => [
{
key: `${prefix ? `${prefix}-` : ""}${userIdentifier}`,
maxRequests: [
{ limit: 5, perTimeIntervalMS: 60 * 1000 },
{ limit: 15, perTimeIntervalMS: 60 * 60 * 1000 },
],
},
{
key: `${prefix ? `${prefix}-` : ""}${ip}`,
maxRequests: [
{ limit: 5, perTimeIntervalMS: 60 * 1000 },
{ limit: 15, perTimeIntervalMS: 60 * 60 * 1000 },
],
},
];

// backend
SuperTokens.init({
appInfo: {
apiDomain: "...",
appName: "...",
websiteDomain: "...",
},
supertokens: {
connectionURI: "...",
},
recipeList: [
EmailPassword.init({
override: {
apis: (originalImplementation) => {
return {
...originalImplementation,
signUpPOST: async function (input) {
// We need to generate a request ID in order to detect possible bots, suspicious IP addresses, etc.
const requestId = (await input.options.req.getJSONBody())
.requestId;
if (!requestId) {
return {
status: "GENERAL_ERROR",
message: "The request ID is required",
};
}

const actionType = "emailpassword-sign-up";
const ip = getIpFromRequest(input.options.req.original);
let email = input.formFields.filter((f) => f.id === "email")[0]
.value as string;
let password = input.formFields.filter(
(f) => f.id === "password",
)[0].value as string;
const bruteForceConfig = getBruteForceConfig(
email,
ip,
actionType,
);

// we check the anomaly detection service before calling the original implementation of signUp
let securityCheckResponse = await handleSecurityChecks({
requestId,
email,
password,
bruteForceConfig,
actionType,
});
if (securityCheckResponse !== undefined) {
return securityCheckResponse;
}

return originalImplementation.signUpPOST!(input);
},
signInPOST: async function (input) {
// We need to generate a request ID in order to detect possible bots, suspicious IP addresses, etc.
const requestId = (await input.options.req.getJSONBody())
.requestId;
if (!requestId) {
return {
status: "GENERAL_ERROR",
message: "The request ID is required",
};
}

const actionType = "emailpassword-sign-in";
const ip = getIpFromRequest(input.options.req.original);
let email = input.formFields.filter((f) => f.id === "email")[0]
.value as string;
const bruteForceConfig = getBruteForceConfig(
email,
ip,
actionType,
);

// we check the anomaly detection service before calling the original implementation of signIn
let securityCheckResponse = await handleSecurityChecks({
requestId,
email,
bruteForceConfig,
actionType,
});
if (securityCheckResponse !== undefined) {
return securityCheckResponse;
}

return originalImplementation.signInPOST!(input);
},
generatePasswordResetTokenPOST: async function (input) {
// We need to generate a request ID in order to detect possible bots, suspicious IP addresses, etc.
const requestId = (await input.options.req.getJSONBody())
.requestId;
if (!requestId) {
return {
status: "GENERAL_ERROR",
message: "The request ID is required",
};
}

const actionType = "send-password-reset-email";
const ip = getIpFromRequest(input.options.req.original);
let email = input.formFields.filter((f) => f.id === "email")[0]
.value as string;
const bruteForceConfig = getBruteForceConfig(
email,
ip,
actionType,
);

// we check the anomaly detection service before calling the original implementation of generatePasswordResetToken
let securityCheckResponse = await handleSecurityChecks({
requestId,
email,
bruteForceConfig,
actionType,
});
if (securityCheckResponse !== undefined) {
return securityCheckResponse;
}

return originalImplementation.generatePasswordResetTokenPOST!(
input,
);
},
passwordResetPOST: async function (input) {
let password = input.formFields.filter(
(f) => f.id === "password",
)[0].value as string;
let securityCheckResponse = await handleSecurityChecks({
password,
});
if (securityCheckResponse !== undefined) {
return securityCheckResponse;
}
return originalImplementation.passwordResetPOST!(input);
},
};
},
},
}),
],
});

The above code overrides the SuperTokens APIs and adding custom logic for anomaly detection. The steps when overriding the APIs are as follows:

  • We get the request ID from the request body. This is a unique ID for the request.
  • Define the action type based on the API you call.
  • We get the email and password from the form fields.
  • We get the IP address from the request.
  • We create the brute force configuration from the email, IP address, and action type. This configuration allows a number of requests over a time interval per:
    1. Action and email/phone number.
    2. Action and IP address.
  • We call the anomaly detection service to check if the request is permissible.
  • If the request is not allowed, it returns a descriptive error response.
  • If the request is permissible, it calls the original implementation of the API.
  • We return the response from the original implementation of the API.

Passwordless

import SuperTokens from "supertokens-node";
import Passwordless from "supertokens-node/recipe/passwordless";
import axios from "axios";
import { createHash } from "crypto";

const SECRET_API_KEY = "<secret-api-key>"; // Your secret API key that you received from the SuperTokens team
const ANOMALY_DETECTION_API_URL =
"https://security-us-east-1.aws.supertokens.io/v1/security";

async function handleSecurityChecks(input: {
actionType?: string;
email?: string;
phoneNumber?: string;
bruteForceConfig?: {
key: string;
maxRequests: {
limit: number;
perTimeIntervalMS: number;
}[];
}[];
}): Promise<
| {
status: "GENERAL_ERROR";
message: string;
}
| undefined
> {
let requestBody: {
email?: string;
phoneNumber?: string;
actionType?: string;
bruteForce?: {
key: string;
maxRequests: {
limit: number;
perTimeIntervalMS: number;
}[];
}[];
} = {};

requestBody.bruteForce = input.bruteForceConfig;
requestBody.email = input.email;
requestBody.phoneNumber = input.phoneNumber;
requestBody.actionType = input.actionType;

let response;
try {
response = await axios.post(ANOMALY_DETECTION_API_URL, requestBody, {
headers: {
Authorization: `Bearer ${SECRET_API_KEY}`,
"Content-Type": "application/json",
},
});
} catch (err) {
// silently fail in order to not break the auth flow
console.error(err);
return;
}
let responseData = response.data;

if (responseData.bruteForce.detected) {
return {
status: "GENERAL_ERROR",
message: "Too many requests. Please try again later.",
};
}

return undefined;
}

function getIpFromRequest(req: Request): string {
let headers: { [key: string]: string } = {};
for (let key of Object.keys(req.headers)) {
headers[key] = (req as any).headers[key]!;
}
return (req as any).headers["x-forwarded-for"] || "127.0.0.1";
}

const getBruteForceConfig = (
userIdentifier: string,
ip: string,
prefix?: string,
) => [
{
key: `${prefix ? `${prefix}-` : ""}${userIdentifier}`,
maxRequests: [
{ limit: 5, perTimeIntervalMS: 60 * 1000 },
{ limit: 15, perTimeIntervalMS: 60 * 60 * 1000 },
],
},
{
key: `${prefix ? `${prefix}-` : ""}${ip}`,
maxRequests: [
{ limit: 5, perTimeIntervalMS: 60 * 1000 },
{ limit: 15, perTimeIntervalMS: 60 * 60 * 1000 },
],
},
];

SuperTokens.init({
framework: "...",
appInfo: {
/*...*/
},
recipeList: [
Passwordless.init({
// ... other customisations ...
contactMethod: "EMAIL_OR_PHONE",
flowType: "USER_INPUT_CODE_AND_MAGIC_LINK",
override: {
apis: (originalImplementation) => {
return {
...originalImplementation,
createCodePOST: async function (input) {
const actionType = "passwordless-send-sms";
const ip = getIpFromRequest(input.options.req.original);
const emailOrPhoneNumber =
"email" in input ? input.email : input.phoneNumber;
const bruteForceConfig = getBruteForceConfig(
emailOrPhoneNumber,
ip,
actionType,
);

// we check the anomaly detection service before calling the original implementation of createCodePOST
let securityCheckResponse = await handleSecurityChecks({
bruteForceConfig,
actionType,
});
if (securityCheckResponse !== undefined) {
return securityCheckResponse;
}

return originalImplementation.createCodePOST!(input);
},
resendCodePOST: async function (input) {
const actionType = "passwordless-send-sms";
const ip = getIpFromRequest(input.options.req.original);
let codesInfo = await Passwordless.listCodesByPreAuthSessionId({
tenantId: input.tenantId,
preAuthSessionId: input.preAuthSessionId,
});
const phoneNumber =
codesInfo && "phoneNumber" in codesInfo
? codesInfo.phoneNumber
: undefined;
const email =
codesInfo && "email" in codesInfo ? codesInfo.email : undefined;
const userIdentifier = email || phoneNumber || input.deviceId;

const bruteForceConfig = getBruteForceConfig(
userIdentifier,
ip,
actionType,
);

// we check the anomaly detection service before calling the original implementation of resendCodePOST
let securityCheckResponse = await handleSecurityChecks({
phoneNumber,
email,
bruteForceConfig,
actionType,
});
if (securityCheckResponse !== undefined) {
return securityCheckResponse;
}

return originalImplementation.resendCodePOST!(input);
},
};
},
},
}),
],
});

The above code overrides the SuperTokens APIs and adding custom logic for anomaly detection. The steps when overriding the APIs are as follows:

  • Define the action type based on the API you call.
  • We get the email or the phone number from the form fields.
  • We get the IP address from the request.
  • We create the brute force configuration from the email, IP address, and action type. This configuration allows a number of requests over a time interval per:
    1. Action and email/phone number.
    2. Action and IP address.
  • The anomaly detection service checks if the request passes the allowed criteria (only brute force detection occurs here).
  • If the request is not allowed, the system returns a descriptive error response.
  • If the request passes the allowed criteria, the original implementation of the API executes.
  • We return the response from the original implementation of the API.