Skip to content

Commit

Permalink
Merge pull request #859 from supertokens/fix-anomaly-detection-fronte…
Browse files Browse the repository at this point in the history
…nd-import

Attack protection suite improvements
  • Loading branch information
rishabhpoddar authored Oct 15, 2024
2 parents 0c7aeba + 770c798 commit e8f1c3f
Show file tree
Hide file tree
Showing 3 changed files with 127 additions and 39 deletions.
120 changes: 102 additions & 18 deletions v2/attackprotectionsuite/backend-setup.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -401,6 +401,7 @@ async function handleSecurityChecks(input: {
});
} catch (err) {
// silently fail in order to not break the auth flow
console.error(err);
return;
}

Expand Down Expand Up @@ -505,10 +506,11 @@ SuperTokens.init({
const actionType = 'emailpassword-sign-up';
const ip = getIpFromRequest(input.options.req.original);
let email = input.formFields.filter((f) => f.id === "email")[0].value;
let password = input.formFields.filter((f) => f.id === "password")[0].value;
const bruteForceConfig = getBruteForceConfig(email, ip, actionType);

// we check the anomaly detection service before calling the original implementation of signUp
let securityCheckResponse = await handleSecurityChecks({ ...input, requestId, email, bruteForceConfig, actionType });
let securityCheckResponse = await handleSecurityChecks({ requestId, email, password, bruteForceConfig, actionType });
if(securityCheckResponse !== undefined) {
return securityCheckResponse;
}
Expand All @@ -531,7 +533,7 @@ SuperTokens.init({
const bruteForceConfig = getBruteForceConfig(email, ip, actionType);

// we check the anomaly detection service before calling the original implementation of signIn
let securityCheckResponse = await handleSecurityChecks({ ...input, requestId, email, bruteForceConfig, actionType });
let securityCheckResponse = await handleSecurityChecks({ requestId, email, bruteForceConfig, actionType });
if(securityCheckResponse !== undefined) {
return securityCheckResponse;
}
Expand All @@ -554,12 +556,20 @@ SuperTokens.init({
const bruteForceConfig = getBruteForceConfig(email, ip, actionType);

// we check the anomaly detection service before calling the original implementation of generatePasswordResetToken
let securityCheckResponse = await handleSecurityChecks({ ...input, requestId, email, bruteForceConfig, actionType });
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;
let securityCheckResponse = await handleSecurityChecks({ password });
if (securityCheckResponse !== undefined) {
return securityCheckResponse;
}
return originalImplementation.passwordResetPOST!(input);
}
}
}
Expand Down Expand Up @@ -888,7 +898,7 @@ func main() {
return resp, nil
}

// rewrite the original implementation of SignInPOST
// rewrite the original implementation of GeneratePasswordResetTokenPOST
originalGeneratePasswordResetTokenPOST := *originalImplementation.GeneratePasswordResetTokenPOST
(*originalImplementation.GeneratePasswordResetTokenPOST) = func(formFields []epmodels.TypeFormField, tenantId string, options epmodels.APIOptions, userContext supertokens.UserContext) (epmodels.GeneratePasswordResetTokenPOSTResponse, error) {
// Generate request ID for bot and suspicious IP detection
Expand Down Expand Up @@ -949,6 +959,46 @@ func main() {
return resp, nil
}

// rewrite the original implementation of PasswordResetPOST
originalPasswordResetPOST := *originalImplementation.PasswordResetPOST
(*originalImplementation.PasswordResetPOST) = func(formFields []epmodels.TypeFormField, token string, tenantId string, options epmodels.APIOptions, userContext supertokens.UserContext) (epmodels.ResetPasswordPOSTResponse, error) {
password := ""
for _, field := range formFields {
if field.ID == "password" {
valueAsString, asStrOk := field.Value.(string)
if !asStrOk {
return epmodels.ResetPasswordPOSTResponse{}, errors.New("Should never come here as we check the type during validation")
}
password = valueAsString
}
}

// Check anomaly detection service before proceeding
checkErr, err := handleSecurityChecks(
SecurityCheckInput{
Password: password,
},
)
if err != nil {
return epmodels.ResetPasswordPOSTResponse{}, err
}

if checkErr != nil {
return epmodels.ResetPasswordPOSTResponse{
GeneralError: checkErr,
}, nil
}

// First we call the original implementation
resp, err := originalPasswordResetPOST(formFields, token, tenantId, options, userContext)

if err != nil {
return epmodels.ResetPasswordPOSTResponse{}, err
}

return resp, nil
}

return originalImplementation
},
Functions: func(originalImplementation epmodels.RecipeInterface) epmodels.RecipeInterface {
Expand Down Expand Up @@ -993,7 +1043,7 @@ SECRET_API_KEY = "<secret-api-key>"; # Your secret API key that you received fro
# The full URL with the correct region will be provided by the SuperTokens team
ANOMALY_DETECTION_API_URL = "https://security-<region>.aws.supertokens.io/v1/security"

async def handle_security_checks(request_id: Union[str, None], password: Union[str, None], brute_force_config: List[Dict[str, Any]], email: Union[str, None], phone_number: Union[str, None], action_type: str) -> Union[GeneralErrorResponse, None]:
async def handle_security_checks(request_id: Union[str, None], password: Union[str, None], brute_force_config: Union[List[Dict[str, Any]], None], email: Union[str, None], phone_number: Union[str, None], action_type: Union[str, None]) -> Union[GeneralErrorResponse, None]:
request_body = {}

if request_id is not None:
Expand Down Expand Up @@ -1123,7 +1173,7 @@ def override_email_password_apis(original_implementation: APIInterface):
email = field.value
brute_force_config = get_brute_force_config(email, ip, action_type)

# we check the anomaly detection service before calling the original implementation of signUp
# we check the anomaly detection service before calling the original implementation of sign_in_post
security_check_response = await handle_security_checks(
request_id=request_id,
password=None,
Expand All @@ -1135,7 +1185,7 @@ def override_email_password_apis(original_implementation: APIInterface):
if security_check_response is not None:
return security_check_response

# We need to call the original implementation of sign_up_post.
# We need to call the original implementation of sign_in_post.
response = await original_sign_in_post(form_fields, tenant_id, api_options, user_context)

return response
Expand All @@ -1159,7 +1209,7 @@ def override_email_password_apis(original_implementation: APIInterface):
email = field.value
brute_force_config = get_brute_force_config(email, ip, action_type)

# we check the anomaly detection service before calling the original implementation of signUp
# we check the anomaly detection service before calling the original implementation of generate_password_reset_token_post
security_check_response = await handle_security_checks(
request_id=request_id,
password=None,
Expand All @@ -1171,12 +1221,45 @@ def override_email_password_apis(original_implementation: APIInterface):
if security_check_response is not None:
return security_check_response

# We need to call the original implementation of sign_up_post.
# We need to call the original implementation of generate_password_reset_token_post.
response = await original_generate_password_reset_token_post(form_fields, tenant_id, api_options, user_context)

return response
original_implementation.generate_password_reset_token_post = generate_password_reset_token_post


original_password_reset_post = original_implementation.password_reset_post
async def password_reset_post(
form_fields: List[FormField],
token: str,
tenant_id: str,
api_options: APIOptions,
user_context: Dict[str, Any],
):
password = None
for field in form_fields:
if field.id == "password":
password = field.value

# we check the anomaly detection service before calling the original implementation of password_reset_post
security_check_response = await handle_security_checks(
request_id=None,
password=password,
brute_force_config=None,
email=None,
phone_number=None,
action_type=None
)
if security_check_response is not None:
return security_check_response

response = await original_password_reset_post(
form_fields, token, tenant_id, api_options, user_context
)

return response
original_implementation.password_reset_post = password_reset_post

return original_implementation
# highlight-end

Expand Down Expand Up @@ -1276,6 +1359,7 @@ async function handleSecurityChecks(input: {
});
} catch (err) {
// silently fail in order to not break the auth flow
console.error(err);
return;
}
let responseData = response.data;
Expand Down Expand Up @@ -1336,8 +1420,8 @@ SuperTokens.init({
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 signUp
let securityCheckResponse = await handleSecurityChecks({ ...input, bruteForceConfig, 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;
}
Expand All @@ -1357,8 +1441,8 @@ SuperTokens.init({

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

// we check the anomaly detection service before calling the original implementation of signUp
let securityCheckResponse = await handleSecurityChecks({ ...input, phoneNumber, email, bruteForceConfig, 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;
}
Expand Down Expand Up @@ -1631,7 +1715,7 @@ SECRET_API_KEY = "<secret-api-key>" # Your secret API key that you received from
# The full URL with the correct region will be provided by the SuperTokens team
ANOMALY_DETECTION_API_URL = "https://security-<region>.aws.supertokens.io/v1/security"

async def handle_security_checks(request_id: Union[str, None], password: Union[str, None], brute_force_config: List[Dict[str, Any]], email: Union[str, None], phone_number: Union[str, None], action_type: str) -> Union[GeneralErrorResponse, None]:
async def handle_security_checks(request_id: Union[str, None], password: Union[str, None], brute_force_config: Union[List[Dict[str, Any]], None], email: Union[str, None], phone_number: Union[str, None], action_type: Union[str, None]) -> Union[GeneralErrorResponse, None]:
request_body = {}

request_body['bruteForce'] = brute_force_config
Expand Down Expand Up @@ -1693,7 +1777,7 @@ def override_passwordless_apis(original_implementation: APIInterface):
identifier = phone_number
brute_force_config = get_brute_force_config(identifier, ip, action_type)

# we check the anomaly detection service before calling the original implementation of signUp
# we check the anomaly detection service before calling the original implementation of create_code_post
security_check_response = await handle_security_checks(
request_id=None,
password=None,
Expand All @@ -1705,7 +1789,7 @@ def override_passwordless_apis(original_implementation: APIInterface):
if security_check_response is not None:
return security_check_response

# We need to call the original implementation of sign_up_post.
# We need to call the original implementation of create_code_post.
response = await original_create_code_post(email, phone_number, tenant_id, api_options, user_context)

return response
Expand All @@ -1728,7 +1812,7 @@ def override_passwordless_apis(original_implementation: APIInterface):
identifier = phone_number
brute_force_config = get_brute_force_config(identifier, ip, action_type)

# we check the anomaly detection service before calling the original implementation of signUp
# we check the anomaly detection service before calling the original implementation of resend_code_post
security_check_response = await handle_security_checks(
request_id=None,
password=None,
Expand All @@ -1740,7 +1824,7 @@ def override_passwordless_apis(original_implementation: APIInterface):
if security_check_response is not None:
return security_check_response

# We need to call the original implementation of sign_up_post.
# We need to call the original implementation of resend_code_post.
response = await original_resend_code_post(device_id, pre_auth_session_id, tenant_id, api_options, user_context)

return response
Expand Down
40 changes: 20 additions & 20 deletions v2/attackprotectionsuite/frontend-setup.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -20,17 +20,15 @@ Below is an example of how to implement request ID generation on your frontend:


```tsx
const PUBLIC_API_KEY = "<public-api-key>"; // Your public API key that you received from the SuperTokens team
const SDK_URL = "https://deviceid.supertokens.io/PqWNQ35Ydhm6WDUK/k9bwGCuvuA83Ad6s";
const PROXY_ENDPOINT_URL = "https://deviceid.supertokens.io/PqWNQ35Ydhm6WDUK/CnsdzKsyFKU8Q3h2"
const ENVIRONMENT_ID = "<environment-id>"; // Your environment ID that you received from the SuperTokens team
// Initialize the agent on page load.
const supertokensRequestIdPromise = import(SDK_URL + "?apiKey=" + PUBLIC_API_KEY).then((RequestId: any) => RequestId.load({
endpoint: [
PROXY_ENDPOINT_URL,
RequestId.defaultEndpoint
]
}));
// 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;
Expand All @@ -43,6 +41,10 @@ async function getRequestId() {
}
```

:::note
Make sure to replace the `<PUBLIC_API_KEY>` in the above string with the provided public API key.
:::

### Passing the Request ID to the Backend

Once you have generated the request ID on the frontend, you need to pass it to the backend. This is done by including the `requestId` property along with the value as part of the preAPIHook body from the initialisation of the recipes.
Expand All @@ -55,17 +57,15 @@ Below is a full example of how to configure the SDK and pass the request ID to t
```tsx
import EmailPassword from "supertokens-auth-react/recipe/emailpassword";

const PUBLIC_API_KEY = "<public-api-key>"; // Your public API key that you received from the SuperTokens team
const SDK_URL = "https://deviceid.supertokens.io/PqWNQ35Ydhm6WDUK/k9bwGCuvuA83Ad6s";
const PROXY_ENDPOINT_URL = "https://deviceid.supertokens.io/PqWNQ35Ydhm6WDUK/CnsdzKsyFKU8Q3h2"
const ENVIRONMENT_ID = "<environment-id>"; // Your environment ID that you received from the SuperTokens team
// Initialize the agent on page load.
const supertokensRequestIdPromise = import(SDK_URL + "?apiKey=" + PUBLIC_API_KEY).then((RequestId: any) => RequestId.load({
endpoint: [
PROXY_ENDPOINT_URL,
RequestId.defaultEndpoint
]
}));
// 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;
Expand Down
6 changes: 5 additions & 1 deletion v2/src/plugins/codeTypeChecking/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -369,7 +369,11 @@ Enabled: true,

if (language === "typescript") {
if (codeSnippet.includes("require(")) {
throw new Error("Do not use 'require' in TS code. Error in " + mdFile);
// except for the attack protection suite , where we need to allow require for compatibility reasons,
// as the SDK URL is dynamic and import might break some builds
if (!codeSnippet.includes('require("https://deviceid.supertokens.io')) {
throw new Error("Do not use 'require' in TS code. Error in " + mdFile);
}
}
codeSnippet = `export { }\n// Original: ${mdFile}\n${codeSnippet}`; // see https://www.aritsltd.com/blog/frontend-development/cannot-redeclare-block-scoped-variable-the-reason-behind-the-error-and-the-way-to-resolve-it/

Expand Down

0 comments on commit e8f1c3f

Please sign in to comment.