From 5eb92f25c9e421b80565afdb96b6d64e8a26b6fd Mon Sep 17 00:00:00 2001 From: rishabhpoddar Date: Tue, 11 Jun 2024 16:46:40 +0530 Subject: [PATCH 01/14] types changes for ep recipe interface (code not compiling) --- lib/ts/recipe/emailpassword/types.ts | 51 +++++++++++++++++++++++++++- 1 file changed, 50 insertions(+), 1 deletion(-) diff --git a/lib/ts/recipe/emailpassword/types.ts b/lib/ts/recipe/emailpassword/types.ts index ee8b481df..fc0a13d50 100644 --- a/lib/ts/recipe/emailpassword/types.ts +++ b/lib/ts/recipe/emailpassword/types.ts @@ -89,6 +89,10 @@ export type RecipeInterface = { password: string; session: SessionContainerInterface | undefined; tenantId: string; + securityOptions: { + enforceEmailBan: boolean; + checkBreachedPassword: boolean; + }; userContext: UserContext; }): Promise< | { @@ -105,6 +109,9 @@ export type RecipeInterface = { | "ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR" | "SESSION_USER_ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR"; } + | { + status: "EMAIL_BANNED_ERROR" | "BREACHED_PASSWORD_ERROR"; + } >; // this function is meant only for creating the recipe in the core and nothing else. // we added this even though signUp exists cause devs may override signup expecting it @@ -114,6 +121,10 @@ export type RecipeInterface = { email: string; password: string; tenantId: string; + securityOptions: { + enforceEmailBan: boolean; + checkBreachedPassword: boolean; + }; userContext: UserContext; }): Promise< | { @@ -122,6 +133,9 @@ export type RecipeInterface = { recipeUserId: RecipeUserId; } | { status: "EMAIL_ALREADY_EXISTS_ERROR" } + | { + status: "EMAIL_BANNED_ERROR" | "BREACHED_PASSWORD_ERROR"; + } >; signIn(input: { @@ -129,10 +143,20 @@ export type RecipeInterface = { password: string; session: SessionContainerInterface | undefined; tenantId: string; + securityOptions: { + enforceUserBan: boolean; + enforceEmailBan: boolean; + checkBreachedPassword: boolean; + limitWrongPasswordAttempts: { + enabled: boolean; + counterKey?: string; // by default will be email, so that the counter is per email, but users can customize it to be something else, like email + IP if they want. + maxNumberOfAttempts?: number; // by default will be 4 + }; + }; userContext: UserContext; }): Promise< | { status: "OK"; user: User; recipeUserId: RecipeUserId } - | { status: "WRONG_CREDENTIALS_ERROR" } + | { status: "WRONG_CREDENTIALS_ERROR"; numberOfIncorrectAttemptsSoFar: number } | { status: "LINKING_TO_SESSION_USER_FAILED"; reason: @@ -141,8 +165,25 @@ export type RecipeInterface = { | "ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR" | "SESSION_USER_ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR"; } + | { + status: "EMAIL_BANNED_ERROR" | "BREACHED_PASSWORD_ERROR"; + } + | { + status: "USER_BANNED"; + user: User; + recipeUserId: RecipeUserId; + } + | { + status: "WRONG_CREDENTIALS_LIMIT_REACHED_ERROR"; + lastLoginAttemptTime: number; // this can be used to reset the timer and try again. + } >; + resetWrongCredentialsCounter(input: { + email: string; + tenantId: string; + }): Promise<{ status: "OK" | "UNKNOWN_EMAIL_ERROR" }>; + verifyCredentials(input: { email: string; password: string; @@ -183,6 +224,13 @@ export type RecipeInterface = { password?: string; userContext: UserContext; applyPasswordPolicy?: boolean; + securityOptions: { + checkBreachedPassword: boolean; + limitOldPasswordReuse: { + enabled: boolean; + numberOfOldPasswordsToCheck?: number; // can be infinity by default + }; + }; tenantIdForPasswordPolicy: string; }): Promise< | { @@ -193,6 +241,7 @@ export type RecipeInterface = { reason: string; } | { status: "PASSWORD_POLICY_VIOLATED_ERROR"; failureReason: string } + | { status: "BREACHED_PASSWORD_ERROR" | "OLD_PASSWORD_REUSED_ERROR" } >; }; From dabae704421da15c47ec81dd5ae9ffd2a747e246 Mon Sep 17 00:00:00 2001 From: rishabhpoddar Date: Tue, 11 Jun 2024 17:13:21 +0530 Subject: [PATCH 02/14] more changes --- lib/ts/recipe/emailpassword/types.ts | 39 +++++++++++++++++++++++++--- 1 file changed, 35 insertions(+), 4 deletions(-) diff --git a/lib/ts/recipe/emailpassword/types.ts b/lib/ts/recipe/emailpassword/types.ts index fc0a13d50..0bbf6a90e 100644 --- a/lib/ts/recipe/emailpassword/types.ts +++ b/lib/ts/recipe/emailpassword/types.ts @@ -91,6 +91,10 @@ export type RecipeInterface = { tenantId: string; securityOptions: { enforceEmailBan: boolean; + ipBan: { + enabled: boolean; + ipAddress: string; + }; checkBreachedPassword: boolean; }; userContext: UserContext; @@ -110,7 +114,7 @@ export type RecipeInterface = { | "SESSION_USER_ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR"; } | { - status: "EMAIL_BANNED_ERROR" | "BREACHED_PASSWORD_ERROR"; + status: "EMAIL_BANNED_ERROR" | "BREACHED_PASSWORD_ERROR" | "IP_BANNED_ERROR"; } >; // this function is meant only for creating the recipe in the core and nothing else. @@ -124,6 +128,10 @@ export type RecipeInterface = { securityOptions: { enforceEmailBan: boolean; checkBreachedPassword: boolean; + ipBan: { + enabled: boolean; + ipAddress: string; + }; }; userContext: UserContext; }): Promise< @@ -134,7 +142,7 @@ export type RecipeInterface = { } | { status: "EMAIL_ALREADY_EXISTS_ERROR" } | { - status: "EMAIL_BANNED_ERROR" | "BREACHED_PASSWORD_ERROR"; + status: "EMAIL_BANNED_ERROR" | "BREACHED_PASSWORD_ERROR" | "IP_BANNED_ERROR"; } >; @@ -146,6 +154,10 @@ export type RecipeInterface = { securityOptions: { enforceUserBan: boolean; enforceEmailBan: boolean; + ipBan: { + enabled: boolean; + ipAddress: string; + }; checkBreachedPassword: boolean; limitWrongPasswordAttempts: { enabled: boolean; @@ -166,7 +178,7 @@ export type RecipeInterface = { | "SESSION_USER_ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR"; } | { - status: "EMAIL_BANNED_ERROR" | "BREACHED_PASSWORD_ERROR"; + status: "EMAIL_BANNED_ERROR" | "BREACHED_PASSWORD_ERROR" | "IP_BANNED_ERROR"; } | { status: "USER_BANNED"; @@ -200,8 +212,27 @@ export type RecipeInterface = { userId: string; // the id can be either recipeUserId or primaryUserId email: string; tenantId: string; + securityOptions: { + enforceUserBan: boolean; + enforceEmailBan: boolean; + ipBan: { + enabled: boolean; + ipAddress: string; + }; + }; userContext: UserContext; - }): Promise<{ status: "OK"; token: string } | { status: "UNKNOWN_USER_ID_ERROR" }>; + }): Promise< + | { status: "OK"; token: string } + | { status: "UNKNOWN_USER_ID_ERROR" } + | { + status: "EMAIL_BANNED_ERROR" | "IP_BANNED_ERROR"; + } + | { + status: "USER_BANNED"; + user: User; + recipeUserId: RecipeUserId; + } + >; consumePasswordResetToken(input: { token: string; From 26b9ae88a9d865d7d6a91f348adf9530f9469164 Mon Sep 17 00:00:00 2001 From: rishabhpoddar Date: Mon, 1 Jul 2024 10:34:22 +0530 Subject: [PATCH 03/14] modifes types of emailpassword recipe --- lib/ts/recipe/emailpassword/types.ts | 117 +++++++++++++++++---------- 1 file changed, 74 insertions(+), 43 deletions(-) diff --git a/lib/ts/recipe/emailpassword/types.ts b/lib/ts/recipe/emailpassword/types.ts index 0bbf6a90e..b0f45c742 100644 --- a/lib/ts/recipe/emailpassword/types.ts +++ b/lib/ts/recipe/emailpassword/types.ts @@ -89,13 +89,13 @@ export type RecipeInterface = { password: string; session: SessionContainerInterface | undefined; tenantId: string; - securityOptions: { - enforceEmailBan: boolean; - ipBan: { - enabled: boolean; - ipAddress: string; + securityOptions?: { + enforceEmailBan?: boolean; + ipBan?: { + enabled?: boolean; + ipAddress?: string; }; - checkBreachedPassword: boolean; + checkBreachedPassword?: boolean; }; userContext: UserContext; }): Promise< @@ -116,6 +116,11 @@ export type RecipeInterface = { | { status: "EMAIL_BANNED_ERROR" | "BREACHED_PASSWORD_ERROR" | "IP_BANNED_ERROR"; } + | { + // this can happen during account linking, if the primary user that this is going to be linked to is banned. + status: "USER_BANNED_ERROR"; + user: User; + } >; // this function is meant only for creating the recipe in the core and nothing else. // we added this even though signUp exists cause devs may override signup expecting it @@ -125,12 +130,12 @@ export type RecipeInterface = { email: string; password: string; tenantId: string; - securityOptions: { - enforceEmailBan: boolean; - checkBreachedPassword: boolean; - ipBan: { - enabled: boolean; - ipAddress: string; + securityOptions?: { + enforceEmailBan?: boolean; + checkBreachedPassword?: boolean; + ipBan?: { + enabled?: boolean; + ipAddress?: string; }; }; userContext: UserContext; @@ -151,18 +156,19 @@ export type RecipeInterface = { password: string; session: SessionContainerInterface | undefined; tenantId: string; - securityOptions: { - enforceUserBan: boolean; - enforceEmailBan: boolean; - ipBan: { - enabled: boolean; - ipAddress: string; + securityOptions?: { + enforceUserBan?: boolean; + enforceEmailBan?: boolean; + ipBan?: { + enabled?: boolean; + ipAddress?: string; }; - checkBreachedPassword: boolean; - limitWrongPasswordAttempts: { - enabled: boolean; - counterKey?: string; // by default will be email, so that the counter is per email, but users can customize it to be something else, like email + IP if they want. - maxNumberOfAttempts?: number; // by default will be 4 + checkBreachedPassword?: boolean; // will be false here by default even if users want to check breached password + limitWrongCredentialsAttempt?: { + enabled?: boolean; + counterKey?: string; // by default, it is just email ID + maxNumberOfIncorrectAttempts?: number; // by default, it is 4 + lockoutTimeInSeconds?: number; // by default, it is 60 }; }; userContext: UserContext; @@ -181,27 +187,52 @@ export type RecipeInterface = { status: "EMAIL_BANNED_ERROR" | "BREACHED_PASSWORD_ERROR" | "IP_BANNED_ERROR"; } | { - status: "USER_BANNED"; + status: "USER_BANNED_ERROR"; user: User; recipeUserId: RecipeUserId; } | { status: "WRONG_CREDENTIALS_LIMIT_REACHED_ERROR"; - lastLoginAttemptTime: number; // this can be used to reset the timer and try again. + remainingLockingTimeInSeconds: number; } >; - resetWrongCredentialsCounter(input: { - email: string; - tenantId: string; - }): Promise<{ status: "OK" | "UNKNOWN_EMAIL_ERROR" }>; - verifyCredentials(input: { email: string; password: string; tenantId: string; userContext: UserContext; - }): Promise<{ status: "OK"; user: User; recipeUserId: RecipeUserId } | { status: "WRONG_CREDENTIALS_ERROR" }>; + securityOptions?: { + enforceUserBan?: boolean; + enforceEmailBan?: boolean; + ipBan?: { + enabled?: boolean; + ipAddress?: string; + }; + checkBreachedPassword?: boolean; // will be false here by default even if users want to check breached password + limitWrongCredentialsAttempt?: { + enabled?: boolean; + counterKey?: string; // by default, it is just email ID + maxNumberOfIncorrectAttempts?: number; // by default, it is 4 + lockoutTimeInSeconds?: number; // by default, it is 60 + }; + }; + }): Promise< + | { status: "OK"; user: User; recipeUserId: RecipeUserId } + | { status: "WRONG_CREDENTIALS_ERROR" } + | { + status: "EMAIL_BANNED_ERROR" | "BREACHED_PASSWORD_ERROR" | "IP_BANNED_ERROR"; + } + | { + status: "USER_BANNED_ERROR"; + user: User; + recipeUserId: RecipeUserId; + } + | { + status: "WRONG_CREDENTIALS_LIMIT_REACHED_ERROR"; + remainingLockingTimeInSeconds: number; + } + >; /** * We pass in the email as well to this function cause the input userId @@ -212,12 +243,12 @@ export type RecipeInterface = { userId: string; // the id can be either recipeUserId or primaryUserId email: string; tenantId: string; - securityOptions: { - enforceUserBan: boolean; - enforceEmailBan: boolean; - ipBan: { - enabled: boolean; - ipAddress: string; + securityOptions?: { + enforceUserBan?: boolean; + enforceEmailBan?: boolean; + ipBan?: { + enabled?: boolean; + ipAddress?: string; }; }; userContext: UserContext; @@ -228,7 +259,7 @@ export type RecipeInterface = { status: "EMAIL_BANNED_ERROR" | "IP_BANNED_ERROR"; } | { - status: "USER_BANNED"; + status: "USER_BANNED_ERROR"; user: User; recipeUserId: RecipeUserId; } @@ -255,11 +286,11 @@ export type RecipeInterface = { password?: string; userContext: UserContext; applyPasswordPolicy?: boolean; - securityOptions: { - checkBreachedPassword: boolean; - limitOldPasswordReuse: { - enabled: boolean; - numberOfOldPasswordsToCheck?: number; // can be infinity by default + securityOptions?: { + checkBreachedPassword?: boolean; + limitOldPasswordReuse?: { + enabled?: boolean; + numberOfOldPasswordsToCheck?: number; }; }; tenantIdForPasswordPolicy: string; From d1691e1a5fcad463080838fc72b8ad00c2819fc4 Mon Sep 17 00:00:00 2001 From: rishabhpoddar Date: Mon, 1 Jul 2024 15:54:21 +0530 Subject: [PATCH 04/14] adds types for all the override functions --- lib/ts/recipe/emailpassword/types.ts | 4 + lib/ts/types.ts | 179 +++++++++++++++++++++++++++ 2 files changed, 183 insertions(+) diff --git a/lib/ts/recipe/emailpassword/types.ts b/lib/ts/recipe/emailpassword/types.ts index b0f45c742..352192d08 100644 --- a/lib/ts/recipe/emailpassword/types.ts +++ b/lib/ts/recipe/emailpassword/types.ts @@ -337,6 +337,7 @@ export type APIInterface = { generatePasswordResetTokenPOST: | undefined | ((input: { + googleRecaptchaToken?: string; formFields: { id: string; value: string; @@ -358,6 +359,7 @@ export type APIInterface = { passwordResetPOST: | undefined | ((input: { + googleRecaptchaToken?: string; formFields: { id: string; value: string; @@ -382,6 +384,7 @@ export type APIInterface = { signInPOST: | undefined | ((input: { + googleRecaptchaToken?: string; formFields: { id: string; value: string; @@ -409,6 +412,7 @@ export type APIInterface = { signUpPOST: | undefined | ((input: { + googleRecaptchaToken?: string; formFields: { id: string; value: string; diff --git a/lib/ts/types.ts b/lib/ts/types.ts index a90d96f0f..0f9d9c83a 100644 --- a/lib/ts/types.ts +++ b/lib/ts/types.ts @@ -19,6 +19,8 @@ import NormalisedURLPath from "./normalisedURLPath"; import { TypeFramework } from "./framework/types"; import { RecipeLevelUser } from "./recipe/accountlinking/types"; import { BaseRequest } from "./framework"; +import OverrideableBuilder from "supertokens-js-override"; +import { SessionContainer } from "./recipe/session"; declare const __brand: unique symbol; type Brand = { [__brand]: B }; @@ -65,6 +67,183 @@ export type TypeInput = { telemetry?: boolean; isInServerlessEnv?: boolean; debug?: boolean; + security?: { + anomalyServiceAPIKey?: string; // this will be provided by us on our supertokens.com dashboard + rateLimitServiceApiKey?: string; // this will be provided by us on our supertokens.com dashboard (cache wrapper) + googleRecaptcha?: { + // if the user provides both, we will use v2 + v2SecretKey?: string; + v1SecretKey?: string; + }; + override?: ( + originalImplementation: SecurityFunctions, + builder?: OverrideableBuilder + ) => SecurityFunctions; + }; +}; + +export type InfoFromRequest = { + ipAddress?: string; + userAgent?: string; +}; + +export type AnomalyServiceActionTypes = + | "sign-in" + | "sign-up" + | "session-refresh" + | "password-reset" + | "send-email" + | "send-sms" + | "mfa-verify" + | "mfa-setup"; + +export type RiskScores = { + // all values are between 0 and 1, with 1 being highest risk + ipRisk: number; + phoneNumberRisk?: number; + emailRisk?: number; + sessionRisk?: number; + userIdRisk?: number; +}; + +export type SecurityFunctions = { + getInfoFromRequest: (input: { request: BaseRequest; userContext: UserContext }) => InfoFromRequest; + + performGoogleRecaptchaV2: (input: { + infoFromRequest: InfoFromRequest; + clientResponseToken: string; + userContext: UserContext; + }) => Promise; + + performGoogleRecaptchaV1: (input: { + infoFromRequest: InfoFromRequest; + clientResponseToken: string; + userContext: UserContext; + }) => Promise; + + calculateRiskScoreUsingAnomalyService: (input: { + infoFromRequest: InfoFromRequest; + email?: string; + phoneNumber?: string; + sessionHandle?: string; + tenantId: string; + userId?: string; + actionType: AnomalyServiceActionTypes; + userContext: UserContext; + }) => Promise; + + logToAnomalyService: (input: { + infoFromRequest: InfoFromRequest; + email?: string; + phoneNumber?: string; + sessionHandle?: string; + tenantId: string; + userId?: string; + action: AnomalyServiceActionTypes; + success: boolean; // this input is what differentiates this function from the one that generates the risk score. + userContext: UserContext; + }) => void; // we intentionally do not return a promise cause this should be non blocking + + // these are all here and not in the respective recipes cause they are to be applied + // only in the APIs and not in the recipe function. We still can't put them in the API + // cause for third party, we do not have the thirdPartyInfo in the api args in the input. + + // Note that for passwordless, we will call this during createCode. + getRateLimitForEmailPasswordSignIn: (input: { + tenantId: string; + session?: SessionContainer; + email: string; + infoFromRequest: InfoFromRequest; + userContext: UserContext; + }) => + | { + key: string; + millisecondsIntervalBetweenAttempts: number; + } + | undefined; // undefined means no rate limit + getRateLimitForEmailPasswordSignUp: (input: { + tenantId: string; + session?: SessionContainer; + email: string; + infoFromRequest: InfoFromRequest; + userContext: UserContext; + }) => + | { + key: string; + millisecondsIntervalBetweenAttempts: number; + } + | undefined; + getRateLimitForThirdPartySignInUp: (input: { + tenantId: string; + session?: SessionContainer; + thirdPartyId: string; // we intentionally do not give thirdPartyUserId because if we did, we'd have to query the thirdParty provider first + infoFromRequest: InfoFromRequest; + userContext: UserContext; + }) => + | { + key: string; + millisecondsIntervalBetweenAttempts: number; + } + | undefined; + getRateLimitForSendingPasswordlessEmail: (input: { + tenantId: string; + session?: SessionContainer; + email: string; + infoFromRequest: InfoFromRequest; + userContext: UserContext; + }) => + | { + key: string; + millisecondsIntervalBetweenAttempts: number; + } + | undefined; + getRateLimitForSendingPasswordlessSms: (input: { + tenantId: string; + session?: SessionContainer; + phoneNumber: string; + infoFromRequest: InfoFromRequest; + userContext: UserContext; + }) => + | { + key: string; + millisecondsIntervalBetweenAttempts: number; + } + | undefined; + getRateLimitForResetPassword: (input: { + tenantId: string; + email: string; + infoFromRequest: InfoFromRequest; + userContext: UserContext; + }) => + | { + key: string; + millisecondsIntervalBetweenAttempts: number; + } + | undefined; + getRateLimitForVerifyEmail: (input: { + tenantId: string; + session: SessionContainer; + // we intentionally do not pass in the email here cause to fetch that, we'd need to query the core first + infoFromRequest: InfoFromRequest; + userContext: UserContext; + }) => + | { + key: string; + millisecondsIntervalBetweenAttempts: number; + } + | undefined; + + // these are functions to actually query the rate limit service + setRateLimitForKey: (input: { + key: string; + millisecondsIntervalBetweenAttempts: number; + userContext: UserContext; + }) => Promise; + isKeyRateLimited: (input: { + key: string; + millisecondsIntervalBetweenAttempts: number; + userContext: UserContext; + }) => Promise; }; export type NetworkInterceptor = (request: HttpRequest, userContext: UserContext) => HttpRequest; From 40c114b3428b752979d1ef75e4f3fd7cb7ed4847 Mon Sep 17 00:00:00 2001 From: rishabhpoddar Date: Mon, 1 Jul 2024 16:06:58 +0530 Subject: [PATCH 05/14] makes rate limiting an array of keys --- lib/ts/types.ts | 30 +++++++++++++++++------------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/lib/ts/types.ts b/lib/ts/types.ts index 0f9d9c83a..342eb4697 100644 --- a/lib/ts/types.ts +++ b/lib/ts/types.ts @@ -159,7 +159,7 @@ export type SecurityFunctions = { | { key: string; millisecondsIntervalBetweenAttempts: number; - } + }[] // is an array so that we can have multiple checks and fail the api if any one of them fail | undefined; // undefined means no rate limit getRateLimitForEmailPasswordSignUp: (input: { tenantId: string; @@ -171,7 +171,7 @@ export type SecurityFunctions = { | { key: string; millisecondsIntervalBetweenAttempts: number; - } + }[] | undefined; getRateLimitForThirdPartySignInUp: (input: { tenantId: string; @@ -183,7 +183,7 @@ export type SecurityFunctions = { | { key: string; millisecondsIntervalBetweenAttempts: number; - } + }[] | undefined; getRateLimitForSendingPasswordlessEmail: (input: { tenantId: string; @@ -195,7 +195,7 @@ export type SecurityFunctions = { | { key: string; millisecondsIntervalBetweenAttempts: number; - } + }[] | undefined; getRateLimitForSendingPasswordlessSms: (input: { tenantId: string; @@ -207,7 +207,7 @@ export type SecurityFunctions = { | { key: string; millisecondsIntervalBetweenAttempts: number; - } + }[] | undefined; getRateLimitForResetPassword: (input: { tenantId: string; @@ -218,7 +218,7 @@ export type SecurityFunctions = { | { key: string; millisecondsIntervalBetweenAttempts: number; - } + }[] | undefined; getRateLimitForVerifyEmail: (input: { tenantId: string; @@ -230,18 +230,22 @@ export type SecurityFunctions = { | { key: string; millisecondsIntervalBetweenAttempts: number; - } + }[] | undefined; // these are functions to actually query the rate limit service setRateLimitForKey: (input: { - key: string; - millisecondsIntervalBetweenAttempts: number; + keys: { + key: string; + millisecondsIntervalBetweenAttempts: number; + }[]; userContext: UserContext; - }) => Promise; - isKeyRateLimited: (input: { - key: string; - millisecondsIntervalBetweenAttempts: number; + }) => void; // should be non blocking, so we do not return a Promise + areAnyKeysRateLimited: (input: { + keys: { + key: string; + millisecondsIntervalBetweenAttempts: number; + }[]; userContext: UserContext; }) => Promise; }; From 9eafc553f64762a61cae3bf0237180d2785bd3b9 Mon Sep 17 00:00:00 2001 From: rishabhpoddar Date: Mon, 1 Jul 2024 20:56:34 +0530 Subject: [PATCH 06/14] adds more type changes --- lib/ts/recipe/passwordless/types.ts | 72 +++++++++++++++++++++++++---- lib/ts/recipe/session/types.ts | 16 ++++++- lib/ts/recipe/thirdparty/types.ts | 17 +++++++ lib/ts/recipe/totp/types.ts | 33 +++++++++++++ lib/ts/types.ts | 12 +++++ 5 files changed, 139 insertions(+), 11 deletions(-) diff --git a/lib/ts/recipe/passwordless/types.ts b/lib/ts/recipe/passwordless/types.ts index 317d809ef..cdb105b4e 100644 --- a/lib/ts/recipe/passwordless/types.ts +++ b/lib/ts/recipe/passwordless/types.ts @@ -121,23 +121,44 @@ export type RecipeInterface = { session: SessionContainerInterface | undefined; tenantId: string; userContext: UserContext; + securityOptions?: { + enforceEmailBan?: boolean; + enforcePhoneNumberBan?: boolean; + ipBan?: { + enabled?: boolean; + ipAddress?: string; + }; + }; } - ) => Promise<{ - status: "OK"; - preAuthSessionId: string; - codeId: string; - deviceId: string; - userInputCode: string; - linkCode: string; - codeLifetime: number; - timeCreated: number; - }>; + ) => Promise< + | { + status: "OK"; + preAuthSessionId: string; + codeId: string; + deviceId: string; + userInputCode: string; + linkCode: string; + codeLifetime: number; + timeCreated: number; + } + | { + status: "EMAIL_BANNED_ERROR" | "PHONE_NUMBER_BANNED" | "IP_BANNED_ERROR"; + } + >; createNewCodeForDevice: (input: { deviceId: string; userInputCode?: string; tenantId: string; userContext: UserContext; + securityOptions?: { + enforceEmailBan?: boolean; + enforcePhoneNumberBan?: boolean; + ipBan?: { + enabled?: boolean; + ipAddress?: string; + }; + }; }) => Promise< | { status: "OK"; @@ -150,6 +171,9 @@ export type RecipeInterface = { timeCreated: number; } | { status: "RESTART_FLOW_ERROR" | "USER_INPUT_CODE_ALREADY_USED_ERROR" } + | { + status: "EMAIL_BANNED_ERROR" | "PHONE_NUMBER_BANNED" | "IP_BANNED_ERROR"; + } >; consumeCode: ( input: @@ -160,6 +184,15 @@ export type RecipeInterface = { session: SessionContainerInterface | undefined; tenantId: string; userContext: UserContext; + securityOptions?: { + enforceUserBan?: boolean; + enforceEmailBan?: boolean; + enforcePhoneNumberBan?: boolean; + ipBan?: { + enabled?: boolean; + ipAddress?: string; + }; + }; } | { linkCode: string; @@ -167,6 +200,15 @@ export type RecipeInterface = { session: SessionContainerInterface | undefined; tenantId: string; userContext: UserContext; + securityOptions?: { + enforceUserBan?: boolean; + enforceEmailBan?: boolean; + enforcePhoneNumberBan?: boolean; + ipBan?: { + enabled?: boolean; + ipAddress?: string; + }; + }; } ) => Promise< | { @@ -195,6 +237,14 @@ export type RecipeInterface = { | "ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR" | "SESSION_USER_ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR"; } + | { + status: "EMAIL_BANNED_ERROR" | "PHONE_NUMBER_BANNED" | "IP_BANNED_ERROR"; + } + | { + status: "USER_BANNED_ERROR"; + user: User; + recipeUserId: RecipeUserId; + } >; checkCode: ( @@ -336,6 +386,7 @@ export type APIInterface = { session: SessionContainerInterface | undefined; options: APIOptions; userContext: UserContext; + googleRecaptchaToken?: string; } ) => Promise< | { @@ -357,6 +408,7 @@ export type APIInterface = { session: SessionContainerInterface | undefined; options: APIOptions; userContext: UserContext; + googleRecaptchaToken?: string; } ) => Promise; diff --git a/lib/ts/recipe/session/types.ts b/lib/ts/recipe/session/types.ts index 169afc31d..fc6c7952d 100644 --- a/lib/ts/recipe/session/types.ts +++ b/lib/ts/recipe/session/types.ts @@ -224,7 +224,15 @@ export type RecipeInterface = { disableAntiCsrf?: boolean; tenantId: string; userContext: UserContext; - }): Promise; + securityOptions?: { + enforceUserBan?: boolean; + }; + }): Promise< + | { status: "OK"; session: SessionContainerInterface } + | { + status: "USER_BANNED_ERROR"; // this will be the case if the primary user id is banned, and not just the recipe user id + } + >; getGlobalClaimValidators(input: { tenantId: string; @@ -241,11 +249,17 @@ export type RecipeInterface = { userContext: UserContext; }): Promise; + // this function will throw unauthorised error in case the user is + // banned - since this function is only to be called with the + // current user's session. refreshSession(input: { refreshToken: string; antiCsrfToken?: string; disableAntiCsrf: boolean; userContext: UserContext; + securityOptions?: { + enforceUserBan?: boolean; + }; }): Promise; /** diff --git a/lib/ts/recipe/thirdparty/types.ts b/lib/ts/recipe/thirdparty/types.ts index a023dd9eb..5f2a716d1 100644 --- a/lib/ts/recipe/thirdparty/types.ts +++ b/lib/ts/recipe/thirdparty/types.ts @@ -175,6 +175,14 @@ export type RecipeInterface = { session: SessionContainerInterface | undefined; tenantId: string; userContext: UserContext; + securityOptions?: { + enforceUserBan?: boolean; + enforceEmailBan?: boolean; + ipBan?: { + enabled?: boolean; + ipAddress?: string; + }; + }; }): Promise< | { status: "OK"; @@ -199,6 +207,14 @@ export type RecipeInterface = { | "ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR" | "SESSION_USER_ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR"; } + | { + status: "EMAIL_BANNED_ERROR" | "IP_BANNED_ERROR"; + } + | { + status: "USER_BANNED_ERROR"; + user: User; + recipeUserId: RecipeUserId; + } >; manuallyCreateOrUpdateUser(input: { @@ -268,6 +284,7 @@ export type APIInterface = { | undefined | (( input: { + googleRecaptchaToken?: string; provider: TypeProvider; tenantId: string; session: SessionContainerInterface | undefined; diff --git a/lib/ts/recipe/totp/types.ts b/lib/ts/recipe/totp/types.ts index 914ff9ec5..89d570680 100644 --- a/lib/ts/recipe/totp/types.ts +++ b/lib/ts/recipe/totp/types.ts @@ -62,6 +62,13 @@ export type RecipeInterface = { skew?: number; period?: number; userContext: UserContext; + securityOptions?: { + enforceUserBan?: boolean; + ipBan?: { + enabled?: boolean; + ipAddress?: string; + }; + }; }) => Promise< | { status: "OK"; @@ -75,6 +82,9 @@ export type RecipeInterface = { | { status: "UNKNOWN_USER_ID_ERROR"; } + | { + status: "IP_BANNED_ERROR" | "USER_BANNED_ERROR"; + } >; updateDevice: (input: { userId: string; @@ -110,6 +120,13 @@ export type RecipeInterface = { deviceName: string; totp: string; userContext: UserContext; + securityOptions?: { + enforceUserBan?: boolean; + ipBan?: { + enabled?: boolean; + ipAddress?: string; + }; + }; }) => Promise< | { status: "OK"; @@ -127,12 +144,22 @@ export type RecipeInterface = { status: "LIMIT_REACHED_ERROR"; retryAfterMs: number; } + | { + status: "IP_BANNED_ERROR" | "USER_BANNED_ERROR"; + } >; verifyTOTP: (input: { tenantId: string; userId: string; totp: string; userContext: UserContext; + securityOptions?: { + enforceUserBan?: boolean; + ipBan?: { + enabled?: boolean; + ipAddress?: string; + }; + }; }) => Promise< | { status: "OK" | "UNKNOWN_USER_ID_ERROR"; @@ -146,6 +173,9 @@ export type RecipeInterface = { status: "LIMIT_REACHED_ERROR"; retryAfterMs: number; } + | { + status: "IP_BANNED_ERROR" | "USER_BANNED_ERROR"; + } >; }; @@ -162,6 +192,7 @@ export type APIInterface = { createDevicePOST: | undefined | ((input: { + googleRecaptchaToken?: string; deviceName?: string; options: APIOptions; session: SessionContainerInterface; @@ -216,6 +247,7 @@ export type APIInterface = { verifyDevicePOST: | undefined | ((input: { + googleRecaptchaToken?: string; deviceName: string; totp: string; options: APIOptions; @@ -244,6 +276,7 @@ export type APIInterface = { verifyTOTPPOST: | undefined | ((input: { + googleRecaptchaToken?: string; totp: string; options: APIOptions; session: SessionContainerInterface; diff --git a/lib/ts/types.ts b/lib/ts/types.ts index 342eb4697..26e95a1f0 100644 --- a/lib/ts/types.ts +++ b/lib/ts/types.ts @@ -248,6 +248,18 @@ export type SecurityFunctions = { }[]; userContext: UserContext; }) => Promise; + + banUser: (input: { + userId: string; // can be a primary or recipe user id, either way, the primary user id is banned + }) => Promise; + + isUserBanned: (input: { + userId: string; // can be a primary or recipe user id, either way, the primary user id is banned + }) => Promise; + + unbanUser: (input: { + userId: string; // can be a primary or recipe user id, either way, the primary user id is unbanned + }) => Promise; }; export type NetworkInterceptor = (request: HttpRequest, userContext: UserContext) => HttpRequest; From 88da59bbdb78c56806cb979b773774212f5707a6 Mon Sep 17 00:00:00 2001 From: rishabhpoddar Date: Tue, 2 Jul 2024 21:58:57 +0530 Subject: [PATCH 07/14] more type changes --- lib/ts/recipe/multifactorauth/types.ts | 2 ++ lib/ts/types.ts | 11 +++++++---- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/lib/ts/recipe/multifactorauth/types.ts b/lib/ts/recipe/multifactorauth/types.ts index a7e340662..f7f71eb6a 100644 --- a/lib/ts/recipe/multifactorauth/types.ts +++ b/lib/ts/recipe/multifactorauth/types.ts @@ -22,6 +22,7 @@ import { SessionContainerInterface } from "../session/types"; import Recipe from "./recipe"; import { TenantConfig } from "../multitenancy/types"; import RecipeUserId from "../../recipeUserId"; +import { RiskScores } from "../../types"; export type MFARequirementList = ( | { @@ -80,6 +81,7 @@ export type RecipeInterface = { requiredSecondaryFactorsForUser: Promise; requiredSecondaryFactorsForTenant: Promise; userContext: UserContext; + riskScores?: RiskScores; }) => Promise | MFARequirementList; markFactorAsCompleteInSession: (input: { diff --git a/lib/ts/types.ts b/lib/ts/types.ts index 26e95a1f0..0a08723f7 100644 --- a/lib/ts/types.ts +++ b/lib/ts/types.ts @@ -68,8 +68,6 @@ export type TypeInput = { isInServerlessEnv?: boolean; debug?: boolean; security?: { - anomalyServiceAPIKey?: string; // this will be provided by us on our supertokens.com dashboard - rateLimitServiceApiKey?: string; // this will be provided by us on our supertokens.com dashboard (cache wrapper) googleRecaptcha?: { // if the user provides both, we will use v2 v2SecretKey?: string; @@ -121,6 +119,10 @@ export type SecurityFunctions = { userContext: UserContext; }) => Promise; + // The apiKey for this will be fetched from the core during the /apiversion API call, and will be saved in memory for use. + // In case /apiversion has not yet been called, this function will call that API first. In case it has been called, but there + // is no API key for this, it means this function will not do anything and return undefined. This also means that if the user + // has added the license key in the core to enable this feature, they will have to restart the backend process once. calculateRiskScoreUsingAnomalyService: (input: { infoFromRequest: InfoFromRequest; email?: string; @@ -130,7 +132,7 @@ export type SecurityFunctions = { userId?: string; actionType: AnomalyServiceActionTypes; userContext: UserContext; - }) => Promise; + }) => Promise; // undefined means we have nothing to return, and we completely ignore this. logToAnomalyService: (input: { infoFromRequest: InfoFromRequest; @@ -233,7 +235,8 @@ export type SecurityFunctions = { }[] | undefined; - // these are functions to actually query the rate limit service + // these are functions to actually query the rate limit service. The api key for this + // will be fetched from the /apiversion API call, similar to the api key for the anomaly service. setRateLimitForKey: (input: { keys: { key: string; From 6d33e181f074dd4b65b784ae29dd41116e11f1e1 Mon Sep 17 00:00:00 2001 From: rishabhpoddar Date: Tue, 16 Jul 2024 14:30:01 +0530 Subject: [PATCH 08/14] fixes a few comments --- lib/ts/recipe/emailpassword/types.ts | 1 - lib/ts/recipe/passwordless/types.ts | 10 +++++++++- lib/ts/recipe/thirdparty/types.ts | 2 +- lib/ts/recipe/totp/types.ts | 1 - lib/ts/types.ts | 14 ++++++++++++++ 5 files changed, 24 insertions(+), 4 deletions(-) diff --git a/lib/ts/recipe/emailpassword/types.ts b/lib/ts/recipe/emailpassword/types.ts index 352192d08..1cf169bb8 100644 --- a/lib/ts/recipe/emailpassword/types.ts +++ b/lib/ts/recipe/emailpassword/types.ts @@ -359,7 +359,6 @@ export type APIInterface = { passwordResetPOST: | undefined | ((input: { - googleRecaptchaToken?: string; formFields: { id: string; value: string; diff --git a/lib/ts/recipe/passwordless/types.ts b/lib/ts/recipe/passwordless/types.ts index cdb105b4e..91d71710d 100644 --- a/lib/ts/recipe/passwordless/types.ts +++ b/lib/ts/recipe/passwordless/types.ts @@ -122,6 +122,7 @@ export type RecipeInterface = { tenantId: string; userContext: UserContext; securityOptions?: { + enforceUserBan?: boolean; // in case this is a sign in and not a sign up enforceEmailBan?: boolean; enforcePhoneNumberBan?: boolean; ipBan?: { @@ -144,6 +145,11 @@ export type RecipeInterface = { | { status: "EMAIL_BANNED_ERROR" | "PHONE_NUMBER_BANNED" | "IP_BANNED_ERROR"; } + | { + status: "USER_BANNED_ERROR"; + user: User; + recipeUserId: RecipeUserId; + } >; createNewCodeForDevice: (input: { @@ -402,13 +408,15 @@ export type APIInterface = { | GeneralErrorResponse >; + // we intentionally do not add googleRecaptcha in here cause + // it's the same device that generates the code during createCode, and if + // that's not a bot, nor is this. resendCodePOST?: ( input: { deviceId: string; preAuthSessionId: string } & { tenantId: string; session: SessionContainerInterface | undefined; options: APIOptions; userContext: UserContext; - googleRecaptchaToken?: string; } ) => Promise; diff --git a/lib/ts/recipe/thirdparty/types.ts b/lib/ts/recipe/thirdparty/types.ts index 5f2a716d1..030f6e367 100644 --- a/lib/ts/recipe/thirdparty/types.ts +++ b/lib/ts/recipe/thirdparty/types.ts @@ -280,11 +280,11 @@ export type APIInterface = { | GeneralErrorResponse >); + // no google recaptcha here cause we reply on the provider to detect bots signInUpPOST: | undefined | (( input: { - googleRecaptchaToken?: string; provider: TypeProvider; tenantId: string; session: SessionContainerInterface | undefined; diff --git a/lib/ts/recipe/totp/types.ts b/lib/ts/recipe/totp/types.ts index 89d570680..8b2c7a8c3 100644 --- a/lib/ts/recipe/totp/types.ts +++ b/lib/ts/recipe/totp/types.ts @@ -192,7 +192,6 @@ export type APIInterface = { createDevicePOST: | undefined | ((input: { - googleRecaptchaToken?: string; deviceName?: string; options: APIOptions; session: SessionContainerInterface; diff --git a/lib/ts/types.ts b/lib/ts/types.ts index 0a08723f7..ccdf0aeb7 100644 --- a/lib/ts/types.ts +++ b/lib/ts/types.ts @@ -107,6 +107,20 @@ export type RiskScores = { export type SecurityFunctions = { getInfoFromRequest: (input: { request: BaseRequest; userContext: UserContext }) => InfoFromRequest; + // this function will return hasProvidedV2SecretKey || hasProvidedV1SecretKey by default. + shouldPerformGoogleRecaptcha: (input: { + hasProvidedV2SecretKey: boolean; + hasProvidedV1SecretKey: boolean; + api: + | "password-reset-code-generation" + | "emailpassword-signin" + | "emailpassword-signup" + | "passwordless-create-code" + | "totp-verify-device" + | "totp-verify-totp"; + userContext: UserContext; + }) => Promise; + performGoogleRecaptchaV2: (input: { infoFromRequest: InfoFromRequest; clientResponseToken: string; From 239e2deb45ce1f1a0aa4304e2892656ba3d34e55 Mon Sep 17 00:00:00 2001 From: rishabhpoddar Date: Tue, 16 Jul 2024 16:37:05 +0530 Subject: [PATCH 09/14] resolves all pr comments --- lib/ts/recipe/emailpassword/types.ts | 48 +-- lib/ts/recipe/passwordless/types.ts | 27 +- lib/ts/recipe/session/types.ts | 47 ++- lib/ts/recipe/thirdparty/types.ts | 6 +- lib/ts/recipe/totp/types.ts | 20 +- lib/ts/types.ts | 492 ++++++++++++++++++++++----- 6 files changed, 492 insertions(+), 148 deletions(-) diff --git a/lib/ts/recipe/emailpassword/types.ts b/lib/ts/recipe/emailpassword/types.ts index 1cf169bb8..f835d4d72 100644 --- a/lib/ts/recipe/emailpassword/types.ts +++ b/lib/ts/recipe/emailpassword/types.ts @@ -91,11 +91,8 @@ export type RecipeInterface = { tenantId: string; securityOptions?: { enforceEmailBan?: boolean; - ipBan?: { - enabled?: boolean; - ipAddress?: string; - }; - checkBreachedPassword?: boolean; + enforceIpBan?: boolean; + ipAddress?: string; }; userContext: UserContext; }): Promise< @@ -114,7 +111,7 @@ export type RecipeInterface = { | "SESSION_USER_ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR"; } | { - status: "EMAIL_BANNED_ERROR" | "BREACHED_PASSWORD_ERROR" | "IP_BANNED_ERROR"; + status: "EMAIL_BANNED_ERROR" | "IP_BANNED_ERROR"; } | { // this can happen during account linking, if the primary user that this is going to be linked to is banned. @@ -132,11 +129,8 @@ export type RecipeInterface = { tenantId: string; securityOptions?: { enforceEmailBan?: boolean; - checkBreachedPassword?: boolean; - ipBan?: { - enabled?: boolean; - ipAddress?: string; - }; + enforceIpBan?: boolean; + ipAddress?: string; }; userContext: UserContext; }): Promise< @@ -147,7 +141,7 @@ export type RecipeInterface = { } | { status: "EMAIL_ALREADY_EXISTS_ERROR" } | { - status: "EMAIL_BANNED_ERROR" | "BREACHED_PASSWORD_ERROR" | "IP_BANNED_ERROR"; + status: "EMAIL_BANNED_ERROR" | "IP_BANNED_ERROR"; } >; @@ -159,11 +153,8 @@ export type RecipeInterface = { securityOptions?: { enforceUserBan?: boolean; enforceEmailBan?: boolean; - ipBan?: { - enabled?: boolean; - ipAddress?: string; - }; - checkBreachedPassword?: boolean; // will be false here by default even if users want to check breached password + enforceIpBan?: boolean; + ipAddress?: string; limitWrongCredentialsAttempt?: { enabled?: boolean; counterKey?: string; // by default, it is just email ID @@ -184,7 +175,7 @@ export type RecipeInterface = { | "SESSION_USER_ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR"; } | { - status: "EMAIL_BANNED_ERROR" | "BREACHED_PASSWORD_ERROR" | "IP_BANNED_ERROR"; + status: "EMAIL_BANNED_ERROR" | "IP_BANNED_ERROR"; } | { status: "USER_BANNED_ERROR"; @@ -205,11 +196,8 @@ export type RecipeInterface = { securityOptions?: { enforceUserBan?: boolean; enforceEmailBan?: boolean; - ipBan?: { - enabled?: boolean; - ipAddress?: string; - }; - checkBreachedPassword?: boolean; // will be false here by default even if users want to check breached password + enforceIpBan?: boolean; + ipAddress?: string; limitWrongCredentialsAttempt?: { enabled?: boolean; counterKey?: string; // by default, it is just email ID @@ -221,7 +209,7 @@ export type RecipeInterface = { | { status: "OK"; user: User; recipeUserId: RecipeUserId } | { status: "WRONG_CREDENTIALS_ERROR" } | { - status: "EMAIL_BANNED_ERROR" | "BREACHED_PASSWORD_ERROR" | "IP_BANNED_ERROR"; + status: "EMAIL_BANNED_ERROR" | "IP_BANNED_ERROR"; } | { status: "USER_BANNED_ERROR"; @@ -246,10 +234,8 @@ export type RecipeInterface = { securityOptions?: { enforceUserBan?: boolean; enforceEmailBan?: boolean; - ipBan?: { - enabled?: boolean; - ipAddress?: string; - }; + enforceIpBan?: boolean; + ipAddress?: string; }; userContext: UserContext; }): Promise< @@ -287,7 +273,6 @@ export type RecipeInterface = { userContext: UserContext; applyPasswordPolicy?: boolean; securityOptions?: { - checkBreachedPassword?: boolean; limitOldPasswordReuse?: { enabled?: boolean; numberOfOldPasswordsToCheck?: number; @@ -303,7 +288,7 @@ export type RecipeInterface = { reason: string; } | { status: "PASSWORD_POLICY_VIOLATED_ERROR"; failureReason: string } - | { status: "BREACHED_PASSWORD_ERROR" | "OLD_PASSWORD_REUSED_ERROR" } + | { status: "OLD_PASSWORD_REUSED_ERROR" } >; }; @@ -338,6 +323,7 @@ export type APIInterface = { | undefined | ((input: { googleRecaptchaToken?: string; + securityServiceRequestId?: string; formFields: { id: string; value: string; @@ -384,6 +370,7 @@ export type APIInterface = { | undefined | ((input: { googleRecaptchaToken?: string; + securityServiceRequestId?: string; formFields: { id: string; value: string; @@ -412,6 +399,7 @@ export type APIInterface = { | undefined | ((input: { googleRecaptchaToken?: string; + securityServiceRequestId?: string; formFields: { id: string; value: string; diff --git a/lib/ts/recipe/passwordless/types.ts b/lib/ts/recipe/passwordless/types.ts index 91d71710d..47f5d6bb2 100644 --- a/lib/ts/recipe/passwordless/types.ts +++ b/lib/ts/recipe/passwordless/types.ts @@ -125,10 +125,8 @@ export type RecipeInterface = { enforceUserBan?: boolean; // in case this is a sign in and not a sign up enforceEmailBan?: boolean; enforcePhoneNumberBan?: boolean; - ipBan?: { - enabled?: boolean; - ipAddress?: string; - }; + enforceIpBan?: boolean; + ipAddress?: string; }; } ) => Promise< @@ -160,10 +158,8 @@ export type RecipeInterface = { securityOptions?: { enforceEmailBan?: boolean; enforcePhoneNumberBan?: boolean; - ipBan?: { - enabled?: boolean; - ipAddress?: string; - }; + enforceIpBan?: boolean; + ipAddress?: string; }; }) => Promise< | { @@ -194,10 +190,8 @@ export type RecipeInterface = { enforceUserBan?: boolean; enforceEmailBan?: boolean; enforcePhoneNumberBan?: boolean; - ipBan?: { - enabled?: boolean; - ipAddress?: string; - }; + enforceIpBan?: boolean; + ipAddress?: string; }; } | { @@ -210,10 +204,8 @@ export type RecipeInterface = { enforceUserBan?: boolean; enforceEmailBan?: boolean; enforcePhoneNumberBan?: boolean; - ipBan?: { - enabled?: boolean; - ipAddress?: string; - }; + enforceIpBan?: boolean; + ipAddress?: string; }; } ) => Promise< @@ -393,6 +385,7 @@ export type APIInterface = { options: APIOptions; userContext: UserContext; googleRecaptchaToken?: string; + securityServiceRequestId?: string; } ) => Promise< | { @@ -408,7 +401,7 @@ export type APIInterface = { | GeneralErrorResponse >; - // we intentionally do not add googleRecaptcha in here cause + // we intentionally do not add googleRecaptcha or securityServiceRequestId in here cause // it's the same device that generates the code during createCode, and if // that's not a bot, nor is this. resendCodePOST?: ( diff --git a/lib/ts/recipe/session/types.ts b/lib/ts/recipe/session/types.ts index 13aece884..ad5772560 100644 --- a/lib/ts/recipe/session/types.ts +++ b/lib/ts/recipe/session/types.ts @@ -210,6 +210,12 @@ export interface VerifySessionOptions { antiCsrfCheck?: boolean; sessionRequired?: boolean; checkDatabase?: boolean; + securityChecks?: { + // should be checked only if checkDatabase is true. + enforceUserBan?: boolean; + enforceIpBan?: boolean; + ipAddress?: string; + }; overrideGlobalClaimValidators?: ( globalClaimValidators: SessionClaimValidator[], session: SessionContainerInterface, @@ -261,6 +267,8 @@ export type RecipeInterface = { userContext: UserContext; securityOptions?: { enforceUserBan?: boolean; + enforceIpBan?: boolean; + ipAddress?: string; }; }): Promise; @@ -296,25 +304,40 @@ export type RecipeInterface = { revokeMultipleSessions(input: { sessionHandles: string[]; userContext: UserContext }): Promise; - // Returns false if the sessionHandle does not exist + // Returns false if the sessionHandle does not exist or security options deny this session updateSessionDataInDatabase(input: { sessionHandle: string; newSessionData: any; + securityOptions?: { + enforceUserBan?: boolean; + enforceIpBan?: boolean; + ipAddress?: string; + }; userContext: UserContext; }): Promise; mergeIntoAccessTokenPayload(input: { sessionHandle: string; accessTokenPayloadUpdate: JSONObject; + securityOptions?: { + enforceUserBan?: boolean; + enforceIpBan?: boolean; + ipAddress?: string; + }; userContext: UserContext; }): Promise; /** - * @returns {Promise} Returns false if the sessionHandle does not exist + * Returns undefined if the sessionHandle does not exist or security options deny this session */ regenerateAccessToken(input: { accessToken: string; newAccessTokenPayload?: any; + securityOptions?: { + enforceUserBan?: boolean; + enforceIpBan?: boolean; + ipAddress?: string; + }; userContext: UserContext; }): Promise< | { @@ -379,7 +402,15 @@ export interface SessionContainerInterface { getSessionDataFromDatabase(userContext?: Record): Promise; - updateSessionDataInDatabase(newSessionData: any, userContext?: Record): Promise; + updateSessionDataInDatabase(input?: { + newSessionData: any; + securityOptions?: { + enforceUserBan?: boolean; + enforceIpBan?: boolean; + ipAddress?: string; + }; + userContext?: Record; + }): Promise; getUserId(userContext?: Record): string; @@ -400,7 +431,15 @@ export interface SessionContainerInterface { getAccessToken(userContext?: Record): string; - mergeIntoAccessTokenPayload(accessTokenPayloadUpdate: JSONObject, userContext?: Record): Promise; + mergeIntoAccessTokenPayload(input?: { + accessTokenPayloadUpdate: JSONObject; + securityOptions?: { + enforceUserBan?: boolean; + enforceIpBan?: boolean; + ipAddress?: string; + }; + userContext?: Record; + }): Promise; getTimeCreated(userContext?: Record): Promise; diff --git a/lib/ts/recipe/thirdparty/types.ts b/lib/ts/recipe/thirdparty/types.ts index 030f6e367..b1027eaed 100644 --- a/lib/ts/recipe/thirdparty/types.ts +++ b/lib/ts/recipe/thirdparty/types.ts @@ -178,10 +178,8 @@ export type RecipeInterface = { securityOptions?: { enforceUserBan?: boolean; enforceEmailBan?: boolean; - ipBan?: { - enabled?: boolean; - ipAddress?: string; - }; + enforceIpBan?: boolean; + ipAddress?: string; }; }): Promise< | { diff --git a/lib/ts/recipe/totp/types.ts b/lib/ts/recipe/totp/types.ts index 8b2c7a8c3..f77d702b0 100644 --- a/lib/ts/recipe/totp/types.ts +++ b/lib/ts/recipe/totp/types.ts @@ -64,10 +64,8 @@ export type RecipeInterface = { userContext: UserContext; securityOptions?: { enforceUserBan?: boolean; - ipBan?: { - enabled?: boolean; - ipAddress?: string; - }; + enforceIpBan?: boolean; + ipAddress?: string; }; }) => Promise< | { @@ -122,10 +120,8 @@ export type RecipeInterface = { userContext: UserContext; securityOptions?: { enforceUserBan?: boolean; - ipBan?: { - enabled?: boolean; - ipAddress?: string; - }; + enforceIpBan?: boolean; + ipAddress?: string; }; }) => Promise< | { @@ -155,10 +151,8 @@ export type RecipeInterface = { userContext: UserContext; securityOptions?: { enforceUserBan?: boolean; - ipBan?: { - enabled?: boolean; - ipAddress?: string; - }; + enforceIpBan?: boolean; + ipAddress?: string; }; }) => Promise< | { @@ -247,6 +241,7 @@ export type APIInterface = { | undefined | ((input: { googleRecaptchaToken?: string; + securityServiceRequestId?: string; deviceName: string; totp: string; options: APIOptions; @@ -276,6 +271,7 @@ export type APIInterface = { | undefined | ((input: { googleRecaptchaToken?: string; + securityServiceRequestId?: string; totp: string; options: APIOptions; session: SessionContainerInterface; diff --git a/lib/ts/types.ts b/lib/ts/types.ts index ccdf0aeb7..5e9bb4e76 100644 --- a/lib/ts/types.ts +++ b/lib/ts/types.ts @@ -73,6 +73,7 @@ export type TypeInput = { v2SecretKey?: string; v1SecretKey?: string; }; + securityServiceApiKey?: string; // this will be used for bruteforce, anomaly, and breached password detection services. override?: ( originalImplementation: SecurityFunctions, builder?: OverrideableBuilder @@ -80,85 +81,361 @@ export type TypeInput = { }; }; -export type InfoFromRequest = { +export type InfoFromRequestHeaders = { ipAddress?: string; userAgent?: string; }; -export type AnomalyServiceActionTypes = - | "sign-in" - | "sign-up" - | "session-refresh" - | "password-reset" - | "send-email" - | "send-sms" - | "mfa-verify" - | "mfa-setup"; +export type SecurityChecksActionTypes = + | "emailpassword-sign-in" + | "emailpassword-sign-up" + | "send-password-reset-email" + | "passwordless-send-email" + | "passwordless-send-sms" + | "totp-verify-device" + | "totp-verify-totp" + | "thirdparty-login" + | "emailverification-send-email"; export type RiskScores = { // all values are between 0 and 1, with 1 being highest risk - ipRisk: number; + requestIdInfo?: + | { + valid: true; + identification: { + data: { + visitorId: string; + requestId: string; + incognito: boolean; + linkedId: string; + tag: Record; + time: string; + timestamp: number; + url: string; + ip: string; + ipLocation: { + accuracyRadius: number; + latitude: number; + longitude: number; + postalCode: string; + timezone: string; + city: { + name: string; + }; + country: { + code: string; + name: string; + }; + continent: { + code: string; + name: string; + }; + subdivisions: Array<{ + isoCode: string; + name: string; + }>; + }; + browserDetails: { + browserName: string; + browserMajorVersion: string; + browserFullVersion: string; + os: string; + osVersion: string; + device: string; + userAgent: string; + }; + confidence: { + score: number; + }; + visitorFound: boolean; + firstSeenAt: { + global: string; + subscription: string; + }; + lastSeenAt: { + global: string | null; + subscription: string | null; + }; + }; + }; + botd: { + data: { + bot: { + result: string; + }; + url: string; + ip: string; + time: string; + userAgent: string; + requestId: string; + }; + }; + rootApps: { + data: { + result: boolean; + }; + }; + emulator: { + data: { + result: boolean; + }; + }; + ipInfo: { + data: { + v4: { + address: string; + geolocation: { + accuracyRadius: number; + latitude: number; + longitude: number; + postalCode: string; + timezone: string; + city: { + name: string; + }; + country: { + code: string; + name: string; + }; + continent: { + code: string; + name: string; + }; + subdivisions: Array<{ + isoCode: string; + name: string; + }>; + }; + asn: { + asn: string; + name: string; + network: string; + }; + datacenter: { + result: boolean; + name: string; + }; + }; + v6: { + address: string; + geolocation: { + accuracyRadius: number; + latitude: number; + longitude: number; + postalCode: string; + timezone: string; + city: { + name: string; + }; + country: { + code: string; + name: string; + }; + continent: { + code: string; + name: string; + }; + subdivisions: Array<{ + isoCode: string; + name: string; + }>; + }; + asn: { + asn: string; + name: string; + network: string; + }; + datacenter: { + result: boolean; + name: string; + }; + }; + }; + }; + ipBlocklist: { + data: { + result: boolean; + details: { + emailSpam: boolean; + attackSource: boolean; + }; + }; + }; + tor: { + data: { + result: boolean; + }; + }; + vpn: { + data: { + result: boolean; + originTimezone: string; + originCountry: string; + methods: { + timezoneMismatch: boolean; + publicVPN: boolean; + auxiliaryMobile: boolean; + osMismatch: boolean; + }; + }; + }; + proxy: { + data: { + result: boolean; + }; + }; + incognito: { + data: { + result: boolean; + }; + }; + tampering: { + data: { + result: boolean; + anomalyScore: number; + }; + }; + clonedApp: { + data: { + result: boolean; + }; + }; + factoryReset: { + data: { + time: string; + timestamp: number; + }; + }; + jailbroken: { + data: { + result: boolean; + }; + }; + frida: { + data: { + result: boolean; + }; + }; + privacySettings: { + data: { + result: boolean; + }; + }; + virtualMachine: { + data: { + result: boolean; + }; + }; + rawDeviceAttributes: { + data: { + architecture: { + value: number; + }; + audio: { + value: number; + }; + canvas: { + value: { + Winding: boolean; + Geometry: string; + Text: string; + }; + }; + colorDepth: { + value: number; + }; + colorGamut: { + value: string; + }; + contrast: { + value: number; + }; + cookiesEnabled: { + value: boolean; + }; + cpuClass: Record; + fonts: { + value: string[]; + }; + }; + }; + highActivity: { + data: { + result: boolean; + }; + }; + locationSpoofing: { + data: { + result: boolean; + }; + }; + remoteControl: { + data: { + result: boolean; + }; + }; + } + | { + valid: false; + }; phoneNumberRisk?: number; emailRisk?: number; - sessionRisk?: number; - userIdRisk?: number; + isBreachedPassword?: boolean; + bruteForce?: + | { + detected: false; + } + | { + detected: true; + key: string; + }; }; export type SecurityFunctions = { - getInfoFromRequest: (input: { request: BaseRequest; userContext: UserContext }) => InfoFromRequest; + getInfoFromRequest: (input: { request: BaseRequest; userContext: UserContext }) => InfoFromRequestHeaders; // this function will return hasProvidedV2SecretKey || hasProvidedV1SecretKey by default. - shouldPerformGoogleRecaptcha: (input: { - hasProvidedV2SecretKey: boolean; - hasProvidedV1SecretKey: boolean; - api: - | "password-reset-code-generation" - | "emailpassword-signin" - | "emailpassword-signup" - | "passwordless-create-code" - | "totp-verify-device" - | "totp-verify-totp"; + shouldEnforceGoogleRecaptchaTokenPresentInRequest: (input: { + tenantId: string; + actionType: SecurityChecksActionTypes; userContext: UserContext; }) => Promise; performGoogleRecaptchaV2: (input: { - infoFromRequest: InfoFromRequest; + infoFromRequest: InfoFromRequestHeaders; clientResponseToken: string; userContext: UserContext; }) => Promise; performGoogleRecaptchaV1: (input: { - infoFromRequest: InfoFromRequest; + infoFromRequest: InfoFromRequestHeaders; clientResponseToken: string; userContext: UserContext; }) => Promise; - // The apiKey for this will be fetched from the core during the /apiversion API call, and will be saved in memory for use. - // In case /apiversion has not yet been called, this function will call that API first. In case it has been called, but there - // is no API key for this, it means this function will not do anything and return undefined. This also means that if the user - // has added the license key in the core to enable this feature, they will have to restart the backend process once. - calculateRiskScoreUsingAnomalyService: (input: { - infoFromRequest: InfoFromRequest; - email?: string; - phoneNumber?: string; - sessionHandle?: string; + // this will return true if securityServiceApiKey is present in the config. + shouldEnforceSecurityServiceRequestIdPresentInRequest: (input: { tenantId: string; - userId?: string; - actionType: AnomalyServiceActionTypes; + actionType: SecurityChecksActionTypes; userContext: UserContext; - }) => Promise; // undefined means we have nothing to return, and we completely ignore this. + }) => Promise; - logToAnomalyService: (input: { - infoFromRequest: InfoFromRequest; + doSecurityChecks: (input: { + infoFromRequestHeaders?: InfoFromRequestHeaders; + passwordHash?: string; // to check against breached password + securityServiceRequestId?: string; email?: string; phoneNumber?: string; - sessionHandle?: string; - tenantId: string; - userId?: string; - action: AnomalyServiceActionTypes; - success: boolean; // this input is what differentiates this function from the one that generates the risk score. + bruteForce?: { + key: string; + maxRequests: { + limit: number; + perTimeIntervalMS: number; + }[]; + }[]; + actionType?: SecurityChecksActionTypes; userContext: UserContext; - }) => void; // we intentionally do not return a promise cause this should be non blocking + }) => Promise; // undefined means we have nothing to return, and we completely ignore this. // these are all here and not in the respective recipes cause they are to be applied // only in the APIs and not in the recipe function. We still can't put them in the API @@ -169,113 +446,166 @@ export type SecurityFunctions = { tenantId: string; session?: SessionContainer; email: string; - infoFromRequest: InfoFromRequest; + infoFromRequest: InfoFromRequestHeaders; userContext: UserContext; }) => | { key: string; - millisecondsIntervalBetweenAttempts: number; + maxRequests: { + limit: number; + perTimeIntervalMS: number; + }[]; }[] // is an array so that we can have multiple checks and fail the api if any one of them fail | undefined; // undefined means no rate limit getRateLimitForEmailPasswordSignUp: (input: { tenantId: string; session?: SessionContainer; email: string; - infoFromRequest: InfoFromRequest; + infoFromRequest: InfoFromRequestHeaders; userContext: UserContext; }) => | { key: string; - millisecondsIntervalBetweenAttempts: number; + maxRequests: { + limit: number; + perTimeIntervalMS: number; + }[]; }[] | undefined; getRateLimitForThirdPartySignInUp: (input: { tenantId: string; session?: SessionContainer; thirdPartyId: string; // we intentionally do not give thirdPartyUserId because if we did, we'd have to query the thirdParty provider first - infoFromRequest: InfoFromRequest; + infoFromRequest: InfoFromRequestHeaders; userContext: UserContext; }) => | { key: string; - millisecondsIntervalBetweenAttempts: number; + maxRequests: { + limit: number; + perTimeIntervalMS: number; + }[]; }[] | undefined; getRateLimitForSendingPasswordlessEmail: (input: { tenantId: string; session?: SessionContainer; email: string; - infoFromRequest: InfoFromRequest; + infoFromRequest: InfoFromRequestHeaders; userContext: UserContext; }) => | { key: string; - millisecondsIntervalBetweenAttempts: number; + maxRequests: { + limit: number; + perTimeIntervalMS: number; + }[]; }[] | undefined; getRateLimitForSendingPasswordlessSms: (input: { tenantId: string; session?: SessionContainer; phoneNumber: string; - infoFromRequest: InfoFromRequest; + infoFromRequest: InfoFromRequestHeaders; userContext: UserContext; }) => | { key: string; - millisecondsIntervalBetweenAttempts: number; + maxRequests: { + limit: number; + perTimeIntervalMS: number; + }[]; }[] | undefined; getRateLimitForResetPassword: (input: { tenantId: string; email: string; - infoFromRequest: InfoFromRequest; + infoFromRequest: InfoFromRequestHeaders; userContext: UserContext; }) => | { key: string; - millisecondsIntervalBetweenAttempts: number; + maxRequests: { + limit: number; + perTimeIntervalMS: number; + }[]; }[] | undefined; getRateLimitForVerifyEmail: (input: { tenantId: string; session: SessionContainer; // we intentionally do not pass in the email here cause to fetch that, we'd need to query the core first - infoFromRequest: InfoFromRequest; + infoFromRequest: InfoFromRequestHeaders; userContext: UserContext; }) => | { key: string; - millisecondsIntervalBetweenAttempts: number; + maxRequests: { + limit: number; + perTimeIntervalMS: number; + }[]; }[] | undefined; - // these are functions to actually query the rate limit service. The api key for this - // will be fetched from the /apiversion API call, similar to the api key for the anomaly service. - setRateLimitForKey: (input: { - keys: { - key: string; - millisecondsIntervalBetweenAttempts: number; - }[]; + getRateLimitForTotpDeviceVerify: (input: { + tenantId: string; + session: SessionContainer; + deviceName: string; + infoFromRequest: InfoFromRequestHeaders; userContext: UserContext; - }) => void; // should be non blocking, so we do not return a Promise - areAnyKeysRateLimited: (input: { - keys: { - key: string; - millisecondsIntervalBetweenAttempts: number; - }[]; + }) => + | { + key: string; + maxRequests: { + limit: number; + perTimeIntervalMS: number; + }[]; + }[] + | undefined; + + getRateLimitForTotpVerify: (input: { + tenantId: string; + session: SessionContainer; + deviceName: string; + infoFromRequest: InfoFromRequestHeaders; userContext: UserContext; - }) => Promise; + }) => + | { + key: string; + maxRequests: { + limit: number; + perTimeIntervalMS: number; + }[]; + }[] + | undefined; - banUser: (input: { - userId: string; // can be a primary or recipe user id, either way, the primary user id is banned + ban: (input: { + userId?: string; // can be a primary or recipe user id, either way, the primary user id is banned + ipAddress?: string; + email?: string; + phoneNumber?: string; + userContext: UserContext; }) => Promise; - isUserBanned: (input: { - userId: string; // can be a primary or recipe user id, either way, the primary user id is banned - }) => Promise; + getIsBanned: (input: { + userId?: string; // can be a primary or recipe user id, either way, the primary user id is banned + ipAddress?: string; + email?: string; + phoneNumber?: string; + userContext: UserContext; + }) => Promise<{ + userIdBanned?: boolean; + ipAddressBanned?: boolean; + emailBanned?: boolean; + phoneNumberBanned?: boolean; + }>; - unbanUser: (input: { - userId: string; // can be a primary or recipe user id, either way, the primary user id is unbanned + unban: (input: { + userId?: string; // can be a primary or recipe user id, either way, the primary user id is banned + ipAddress?: string; + email?: string; + phoneNumber?: string; + userContext: UserContext; }) => Promise; }; From 206700659bd233a892fa4e45066ad4630b22fa07 Mon Sep 17 00:00:00 2001 From: rishabhpoddar Date: Tue, 16 Jul 2024 17:03:38 +0530 Subject: [PATCH 10/14] more changes --- lib/ts/types.ts | 35 +++++++++++++++++++++++++++++++---- 1 file changed, 31 insertions(+), 4 deletions(-) diff --git a/lib/ts/types.ts b/lib/ts/types.ts index 5e9bb4e76..de43f51e1 100644 --- a/lib/ts/types.ts +++ b/lib/ts/types.ts @@ -392,7 +392,11 @@ export type RiskScores = { }; export type SecurityFunctions = { - getInfoFromRequest: (input: { request: BaseRequest; userContext: UserContext }) => InfoFromRequestHeaders; + getInfoFromRequest: (input: { + tenantId: string; + request: BaseRequest; + userContext: UserContext; + }) => InfoFromRequestHeaders; // this function will return hasProvidedV2SecretKey || hasProvidedV1SecretKey by default. shouldEnforceGoogleRecaptchaTokenPresentInRequest: (input: { @@ -402,14 +406,16 @@ export type SecurityFunctions = { }) => Promise; performGoogleRecaptchaV2: (input: { + tenantId: string; infoFromRequest: InfoFromRequestHeaders; - clientResponseToken: string; + googleRecaptchaToken: string; userContext: UserContext; }) => Promise; performGoogleRecaptchaV1: (input: { + tenantId: string; infoFromRequest: InfoFromRequestHeaders; - clientResponseToken: string; + googleRecaptchaToken: string; userContext: UserContext; }) => Promise; @@ -420,7 +426,8 @@ export type SecurityFunctions = { userContext: UserContext; }) => Promise; - doSecurityChecks: (input: { + getRiskScoresFromSecurityService: (input: { + tenantId: string; infoFromRequestHeaders?: InfoFromRequestHeaders; passwordHash?: string; // to check against breached password securityServiceRequestId?: string; @@ -437,6 +444,22 @@ export type SecurityFunctions = { userContext: UserContext; }) => Promise; // undefined means we have nothing to return, and we completely ignore this. + shouldRejectRequestBasedOnRiskScores: (input: { + tenantId: string; + riskScores: RiskScores; + actionType: SecurityChecksActionTypes; + userContext: UserContext; + }) => Promise<{ + rejectBasedOnBruteForce?: boolean; + rejectBasedOnBreachedPassword?: boolean; + rejectBasedOnBotDetection?: boolean; + rejectBasedOnSuspiciousIPOrLocation?: boolean; + rejectBasedOnVPNBeingUsed?: boolean; + rejectBasedOnPhoneNumberRisk?: boolean; + rejectBasedOnEmailRisk?: boolean; + otherReasonForRejection?: string; + }>; + // these are all here and not in the respective recipes cause they are to be applied // only in the APIs and not in the recipe function. We still can't put them in the API // cause for third party, we do not have the thirdPartyInfo in the api args in the input. @@ -579,7 +602,9 @@ export type SecurityFunctions = { }[] | undefined; + // if tenant id is not provided, then we ban across all tenants. ban: (input: { + tenantId?: string; userId?: string; // can be a primary or recipe user id, either way, the primary user id is banned ipAddress?: string; email?: string; @@ -588,6 +613,7 @@ export type SecurityFunctions = { }) => Promise; getIsBanned: (input: { + tenantId?: string; userId?: string; // can be a primary or recipe user id, either way, the primary user id is banned ipAddress?: string; email?: string; @@ -601,6 +627,7 @@ export type SecurityFunctions = { }>; unban: (input: { + tenantId?: string; userId?: string; // can be a primary or recipe user id, either way, the primary user id is banned ipAddress?: string; email?: string; From 402726dd3801011a065726939c87cc34c73ff178 Mon Sep 17 00:00:00 2001 From: rishabhpoddar Date: Tue, 16 Jul 2024 19:18:22 +0530 Subject: [PATCH 11/14] small changes --- lib/ts/recipe/thirdparty/types.ts | 2 +- lib/ts/types.ts | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/ts/recipe/thirdparty/types.ts b/lib/ts/recipe/thirdparty/types.ts index b1027eaed..4063d0e49 100644 --- a/lib/ts/recipe/thirdparty/types.ts +++ b/lib/ts/recipe/thirdparty/types.ts @@ -278,7 +278,7 @@ export type APIInterface = { | GeneralErrorResponse >); - // no google recaptcha here cause we reply on the provider to detect bots + // no google recaptcha or no securityservicetoken here cause we reply on the provider to detect bots and other issues. signInUpPOST: | undefined | (( diff --git a/lib/ts/types.ts b/lib/ts/types.ts index de43f51e1..126315819 100644 --- a/lib/ts/types.ts +++ b/lib/ts/types.ts @@ -426,10 +426,12 @@ export type SecurityFunctions = { userContext: UserContext; }) => Promise; + // we pass in password instead of passwordHash cause maybe users want to use a different way to + // check for breached password. getRiskScoresFromSecurityService: (input: { tenantId: string; infoFromRequestHeaders?: InfoFromRequestHeaders; - passwordHash?: string; // to check against breached password + password?: string; // to check against breached password securityServiceRequestId?: string; email?: string; phoneNumber?: string; From cf3d5811e5ec53170a82934bb3e2f54905c8cb96 Mon Sep 17 00:00:00 2001 From: rishabhpoddar Date: Tue, 16 Jul 2024 23:38:57 +0530 Subject: [PATCH 12/14] removes google recaptcha and security service request id from totp --- lib/ts/recipe/totp/types.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/ts/recipe/totp/types.ts b/lib/ts/recipe/totp/types.ts index f77d702b0..0f951bb76 100644 --- a/lib/ts/recipe/totp/types.ts +++ b/lib/ts/recipe/totp/types.ts @@ -237,11 +237,11 @@ export type APIInterface = { | GeneralErrorResponse >); + // we don't need to pass google recaptcha or security service request id here because the device is already verified + // since this only happens after you have a session token. verifyDevicePOST: | undefined | ((input: { - googleRecaptchaToken?: string; - securityServiceRequestId?: string; deviceName: string; totp: string; options: APIOptions; @@ -267,11 +267,11 @@ export type APIInterface = { | GeneralErrorResponse >); + // we don't need to pass google recaptcha or security service request id here because the device is already verified + // since this only happens after you have a session token. verifyTOTPPOST: | undefined | ((input: { - googleRecaptchaToken?: string; - securityServiceRequestId?: string; totp: string; options: APIOptions; session: SessionContainerInterface; From 2b6e09e73f0ef9029f58af0aaad983a769724997 Mon Sep 17 00:00:00 2001 From: rishabhpoddar Date: Wed, 17 Jul 2024 13:50:27 +0530 Subject: [PATCH 13/14] adds a few more params from security service --- lib/ts/types.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/lib/ts/types.ts b/lib/ts/types.ts index 126315819..c412bd582 100644 --- a/lib/ts/types.ts +++ b/lib/ts/types.ts @@ -381,6 +381,9 @@ export type RiskScores = { phoneNumberRisk?: number; emailRisk?: number; isBreachedPassword?: boolean; + isImpossibleTravel?: boolean; // only during sign in or sign up, based on email / phone number + isNewDevice?: boolean; // based on visitorId being different for the email / phone number + numberOfUniqueDevicesForUser?: number; // based on number of visitorIds mapped to the input email / phone number bruteForce?: | { detected: false; @@ -459,6 +462,9 @@ export type SecurityFunctions = { rejectBasedOnVPNBeingUsed?: boolean; rejectBasedOnPhoneNumberRisk?: boolean; rejectBasedOnEmailRisk?: boolean; + rejectBasedOnImpossibleTravel?: boolean; + rejectBasedOnNewDevice?: boolean; + rejectBasedOnNumberOfUniqueDevicesForUser?: boolean; otherReasonForRejection?: string; }>; From b19c86f3d0f190db3a883672797086d4365f16bf Mon Sep 17 00:00:00 2001 From: rishabhpoddar Date: Fri, 26 Jul 2024 14:00:05 +0530 Subject: [PATCH 14/14] changes output schema of api --- lib/ts/types.ts | 347 +++++++++++++++++------------------------------- 1 file changed, 119 insertions(+), 228 deletions(-) diff --git a/lib/ts/types.ts b/lib/ts/types.ts index c412bd582..13845d0a4 100644 --- a/lib/ts/types.ts +++ b/lib/ts/types.ts @@ -99,21 +99,36 @@ export type SecurityChecksActionTypes = export type RiskScores = { // all values are between 0 and 1, with 1 being highest risk - requestIdInfo?: + requestIdInfo: | { valid: true; identification: { - data: { - visitorId: string; - requestId: string; - incognito: boolean; - linkedId: string; - tag: Record; - time: string; - timestamp: number; - url: string; - ip: string; - ipLocation: { + visitorId: string; + requestId: string; + incognito: boolean; + linkedId: string; + tag: Record; + timeInMS: number; + url: string; + browserDetails: { + browserName: string; + browserMajorVersion: string; + browserFullVersion: string; + os: string; + osVersion: string; + device: string; + userAgent: string; + }; + confidence: { + score: number; + }; + }; + botDetected: boolean; + isEmulator: boolean; + ipInfo: { + v4: { + address: string; + geolocation: { accuracyRadius: number; latitude: number; longitude: number; @@ -135,263 +150,139 @@ export type RiskScores = { name: string; }>; }; - browserDetails: { - browserName: string; - browserMajorVersion: string; - browserFullVersion: string; - os: string; - osVersion: string; - device: string; - userAgent: string; + asn: { + asn: string; + name: string; + network: string; }; - confidence: { - score: number; - }; - visitorFound: boolean; - firstSeenAt: { - global: string; - subscription: string; - }; - lastSeenAt: { - global: string | null; - subscription: string | null; - }; - }; - }; - botd: { - data: { - bot: { - result: string; + datacenter: { + result: boolean; + name: string; }; - url: string; - ip: string; - time: string; - userAgent: string; - requestId: string; }; - }; - rootApps: { - data: { - result: boolean; - }; - }; - emulator: { - data: { - result: boolean; - }; - }; - ipInfo: { - data: { - v4: { - address: string; - geolocation: { - accuracyRadius: number; - latitude: number; - longitude: number; - postalCode: string; - timezone: string; - city: { - name: string; - }; - country: { - code: string; - name: string; - }; - continent: { - code: string; - name: string; - }; - subdivisions: Array<{ - isoCode: string; - name: string; - }>; - }; - asn: { - asn: string; + v6: { + address: string; + geolocation: { + accuracyRadius: number; + latitude: number; + longitude: number; + postalCode: string; + timezone: string; + city: { name: string; - network: string; }; - datacenter: { - result: boolean; + country: { + code: string; name: string; }; - }; - v6: { - address: string; - geolocation: { - accuracyRadius: number; - latitude: number; - longitude: number; - postalCode: string; - timezone: string; - city: { - name: string; - }; - country: { - code: string; - name: string; - }; - continent: { - code: string; - name: string; - }; - subdivisions: Array<{ - isoCode: string; - name: string; - }>; - }; - asn: { - asn: string; + continent: { + code: string; name: string; - network: string; }; - datacenter: { - result: boolean; + subdivisions: Array<{ + isoCode: string; name: string; - }; + }>; }; - }; - }; - ipBlocklist: { - data: { - result: boolean; - details: { - emailSpam: boolean; - attackSource: boolean; + asn: { + asn: string; + name: string; + network: string; }; - }; - }; - tor: { - data: { - result: boolean; - }; - }; - vpn: { - data: { - result: boolean; - originTimezone: string; - originCountry: string; - methods: { - timezoneMismatch: boolean; - publicVPN: boolean; - auxiliaryMobile: boolean; - osMismatch: boolean; + datacenter: { + result: boolean; + name: string; }; }; }; - proxy: { - data: { - result: boolean; + ipBlocklist: { + result: boolean; + details: { + emailSpam: boolean; + attackSource: boolean; }; }; - incognito: { - data: { - result: boolean; + isUsingTor: boolean; + vpn: { + result: boolean; + originTimezone: string; + originCountry: string; + methods: { + timezoneMismatch: boolean; + publicVPN: boolean; + auxiliaryMobile: boolean; + osMismatch: boolean; }; }; + proxy: boolean; + incognito: boolean; tampering: { - data: { - result: boolean; - anomalyScore: number; - }; - }; - clonedApp: { - data: { - result: boolean; - }; + result: boolean; + anomalyScore: number; }; + clonedApp: boolean; factoryReset: { - data: { - time: string; - timestamp: number; - }; + time: string; + timestamp: number; }; - jailbroken: { - data: { - result: boolean; + jailbroken: boolean; + frida: boolean; + privacySettings: boolean; + virtualMachine: boolean; + rawDeviceAttributes: { + architecture: { + value: number; }; - }; - frida: { - data: { - result: boolean; + audio: { + value: number; }; - }; - privacySettings: { - data: { - result: boolean; + canvas: { + value: { + Winding: boolean; + Geometry: string; + Text: string; + }; }; - }; - virtualMachine: { - data: { - result: boolean; + colorDepth: { + value: number; }; - }; - rawDeviceAttributes: { - data: { - architecture: { - value: number; - }; - audio: { - value: number; - }; - canvas: { - value: { - Winding: boolean; - Geometry: string; - Text: string; - }; - }; - colorDepth: { - value: number; - }; - colorGamut: { - value: string; - }; - contrast: { - value: number; - }; - cookiesEnabled: { - value: boolean; - }; - cpuClass: Record; - fonts: { - value: string[]; - }; + colorGamut: { + value: string; }; - }; - highActivity: { - data: { - result: boolean; + contrast: { + value: number; }; - }; - locationSpoofing: { - data: { - result: boolean; + cookiesEnabled: { + value: boolean; }; - }; - remoteControl: { - data: { - result: boolean; + cpuClass: Record; + fonts: { + value: string[]; }; }; + highActivity: boolean; + locationSpoofing: boolean; + remoteControl: boolean; } | { valid: false; - }; - phoneNumberRisk?: number; - emailRisk?: number; - isBreachedPassword?: boolean; - isImpossibleTravel?: boolean; // only during sign in or sign up, based on email / phone number - isNewDevice?: boolean; // based on visitorId being different for the email / phone number - numberOfUniqueDevicesForUser?: number; // based on number of visitorIds mapped to the input email / phone number - bruteForce?: + } + | null; + phoneNumberRisk: number | null; + emailRisk: number | null; + isBreachedPassword: boolean | null; + isImpossibleTravel: boolean | null; // only during sign in or sign up, based on email / phone number + isNewDevice: boolean | null; // based on visitorId being different for the email / phone number + numberOfUniqueDevicesForUser: number | null; // based on number of visitorIds mapped to the input email / phone number + bruteForce: | { detected: false; } | { detected: true; key: string; - }; + } + | null; }; export type SecurityFunctions = {