diff --git a/lib/ts/recipe/emailpassword/types.ts b/lib/ts/recipe/emailpassword/types.ts index ee8b481dfb..f835d4d727 100644 --- a/lib/ts/recipe/emailpassword/types.ts +++ b/lib/ts/recipe/emailpassword/types.ts @@ -89,6 +89,11 @@ export type RecipeInterface = { password: string; session: SessionContainerInterface | undefined; tenantId: string; + securityOptions?: { + enforceEmailBan?: boolean; + enforceIpBan?: boolean; + ipAddress?: string; + }; userContext: UserContext; }): Promise< | { @@ -105,6 +110,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"; + } + | { + // 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 @@ -114,6 +127,11 @@ export type RecipeInterface = { email: string; password: string; tenantId: string; + securityOptions?: { + enforceEmailBan?: boolean; + enforceIpBan?: boolean; + ipAddress?: string; + }; userContext: UserContext; }): Promise< | { @@ -122,6 +140,9 @@ export type RecipeInterface = { recipeUserId: RecipeUserId; } | { status: "EMAIL_ALREADY_EXISTS_ERROR" } + | { + status: "EMAIL_BANNED_ERROR" | "IP_BANNED_ERROR"; + } >; signIn(input: { @@ -129,10 +150,22 @@ export type RecipeInterface = { password: string; session: SessionContainerInterface | undefined; tenantId: string; + securityOptions?: { + enforceUserBan?: boolean; + enforceEmailBan?: boolean; + enforceIpBan?: boolean; + ipAddress?: string; + 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; }): 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,6 +174,18 @@ 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; + } + | { + status: "WRONG_CREDENTIALS_LIMIT_REACHED_ERROR"; + remainingLockingTimeInSeconds: number; + } >; verifyCredentials(input: { @@ -148,7 +193,34 @@ export type RecipeInterface = { password: string; tenantId: string; userContext: UserContext; - }): Promise<{ status: "OK"; user: User; recipeUserId: RecipeUserId } | { status: "WRONG_CREDENTIALS_ERROR" }>; + securityOptions?: { + enforceUserBan?: boolean; + enforceEmailBan?: boolean; + enforceIpBan?: boolean; + ipAddress?: string; + 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" | "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 @@ -159,8 +231,25 @@ export type RecipeInterface = { userId: string; // the id can be either recipeUserId or primaryUserId email: string; tenantId: string; + securityOptions?: { + enforceUserBan?: boolean; + enforceEmailBan?: boolean; + enforceIpBan?: 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_ERROR"; + user: User; + recipeUserId: RecipeUserId; + } + >; consumePasswordResetToken(input: { token: string; @@ -183,6 +272,12 @@ export type RecipeInterface = { password?: string; userContext: UserContext; applyPasswordPolicy?: boolean; + securityOptions?: { + limitOldPasswordReuse?: { + enabled?: boolean; + numberOfOldPasswordsToCheck?: number; + }; + }; tenantIdForPasswordPolicy: string; }): Promise< | { @@ -193,6 +288,7 @@ export type RecipeInterface = { reason: string; } | { status: "PASSWORD_POLICY_VIOLATED_ERROR"; failureReason: string } + | { status: "OLD_PASSWORD_REUSED_ERROR" } >; }; @@ -226,6 +322,8 @@ export type APIInterface = { generatePasswordResetTokenPOST: | undefined | ((input: { + googleRecaptchaToken?: string; + securityServiceRequestId?: string; formFields: { id: string; value: string; @@ -271,6 +369,8 @@ export type APIInterface = { signInPOST: | undefined | ((input: { + googleRecaptchaToken?: string; + securityServiceRequestId?: string; formFields: { id: string; value: string; @@ -298,6 +398,8 @@ export type APIInterface = { signUpPOST: | undefined | ((input: { + googleRecaptchaToken?: string; + securityServiceRequestId?: string; formFields: { id: string; value: string; diff --git a/lib/ts/recipe/multifactorauth/types.ts b/lib/ts/recipe/multifactorauth/types.ts index a7e3406626..f7f71eb6a7 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/recipe/passwordless/types.ts b/lib/ts/recipe/passwordless/types.ts index 317d809ef7..47f5d6bb2f 100644 --- a/lib/ts/recipe/passwordless/types.ts +++ b/lib/ts/recipe/passwordless/types.ts @@ -121,23 +121,46 @@ export type RecipeInterface = { session: SessionContainerInterface | undefined; tenantId: string; userContext: UserContext; + securityOptions?: { + enforceUserBan?: boolean; // in case this is a sign in and not a sign up + enforceEmailBan?: boolean; + enforcePhoneNumberBan?: boolean; + enforceIpBan?: 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"; + } + | { + status: "USER_BANNED_ERROR"; + user: User; + recipeUserId: RecipeUserId; + } + >; createNewCodeForDevice: (input: { deviceId: string; userInputCode?: string; tenantId: string; userContext: UserContext; + securityOptions?: { + enforceEmailBan?: boolean; + enforcePhoneNumberBan?: boolean; + enforceIpBan?: boolean; + ipAddress?: string; + }; }) => Promise< | { status: "OK"; @@ -150,6 +173,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 +186,13 @@ export type RecipeInterface = { session: SessionContainerInterface | undefined; tenantId: string; userContext: UserContext; + securityOptions?: { + enforceUserBan?: boolean; + enforceEmailBan?: boolean; + enforcePhoneNumberBan?: boolean; + enforceIpBan?: boolean; + ipAddress?: string; + }; } | { linkCode: string; @@ -167,6 +200,13 @@ export type RecipeInterface = { session: SessionContainerInterface | undefined; tenantId: string; userContext: UserContext; + securityOptions?: { + enforceUserBan?: boolean; + enforceEmailBan?: boolean; + enforcePhoneNumberBan?: boolean; + enforceIpBan?: boolean; + ipAddress?: string; + }; } ) => Promise< | { @@ -195,6 +235,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 +384,8 @@ export type APIInterface = { session: SessionContainerInterface | undefined; options: APIOptions; userContext: UserContext; + googleRecaptchaToken?: string; + securityServiceRequestId?: string; } ) => Promise< | { @@ -351,6 +401,9 @@ export type APIInterface = { | GeneralErrorResponse >; + // 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?: ( input: { deviceId: string; preAuthSessionId: string } & { tenantId: string; diff --git a/lib/ts/recipe/session/types.ts b/lib/ts/recipe/session/types.ts index 0510b8820f..ad5772560a 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, @@ -226,7 +232,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; @@ -243,11 +257,19 @@ 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; + enforceIpBan?: boolean; + ipAddress?: string; + }; }): Promise; /** @@ -282,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< | { @@ -365,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; @@ -386,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 21c7608da9..558e5578ed 100644 --- a/lib/ts/recipe/thirdparty/types.ts +++ b/lib/ts/recipe/thirdparty/types.ts @@ -176,6 +176,12 @@ export type RecipeInterface = { session: SessionContainerInterface | undefined; tenantId: string; userContext: UserContext; + securityOptions?: { + enforceUserBan?: boolean; + enforceEmailBan?: boolean; + enforceIpBan?: boolean; + ipAddress?: string; + }; }): Promise< | { status: "OK"; @@ -200,6 +206,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: { @@ -265,6 +279,7 @@ export type APIInterface = { | GeneralErrorResponse >); + // 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/recipe/totp/types.ts b/lib/ts/recipe/totp/types.ts index 914ff9ec51..0f951bb766 100644 --- a/lib/ts/recipe/totp/types.ts +++ b/lib/ts/recipe/totp/types.ts @@ -62,6 +62,11 @@ export type RecipeInterface = { skew?: number; period?: number; userContext: UserContext; + securityOptions?: { + enforceUserBan?: boolean; + enforceIpBan?: boolean; + ipAddress?: string; + }; }) => Promise< | { status: "OK"; @@ -75,6 +80,9 @@ export type RecipeInterface = { | { status: "UNKNOWN_USER_ID_ERROR"; } + | { + status: "IP_BANNED_ERROR" | "USER_BANNED_ERROR"; + } >; updateDevice: (input: { userId: string; @@ -110,6 +118,11 @@ export type RecipeInterface = { deviceName: string; totp: string; userContext: UserContext; + securityOptions?: { + enforceUserBan?: boolean; + enforceIpBan?: boolean; + ipAddress?: string; + }; }) => Promise< | { status: "OK"; @@ -127,12 +140,20 @@ 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; + enforceIpBan?: boolean; + ipAddress?: string; + }; }) => Promise< | { status: "OK" | "UNKNOWN_USER_ID_ERROR"; @@ -146,6 +167,9 @@ export type RecipeInterface = { status: "LIMIT_REACHED_ERROR"; retryAfterMs: number; } + | { + status: "IP_BANNED_ERROR" | "USER_BANNED_ERROR"; + } >; }; @@ -213,6 +237,8 @@ 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: { @@ -241,6 +267,8 @@ 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: { diff --git a/lib/ts/types.ts b/lib/ts/types.ts index a90d96f0f7..13845d0a49 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,472 @@ export type TypeInput = { telemetry?: boolean; isInServerlessEnv?: boolean; debug?: boolean; + security?: { + googleRecaptcha?: { + // if the user provides both, we will use v2 + v2SecretKey?: string; + v1SecretKey?: string; + }; + securityServiceApiKey?: string; // this will be used for bruteforce, anomaly, and breached password detection services. + override?: ( + originalImplementation: SecurityFunctions, + builder?: OverrideableBuilder + ) => SecurityFunctions; + }; +}; + +export type InfoFromRequestHeaders = { + ipAddress?: string; + userAgent?: string; +}; + +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 + requestIdInfo: + | { + valid: true; + identification: { + 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; + 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: { + result: boolean; + details: { + emailSpam: boolean; + attackSource: boolean; + }; + }; + isUsingTor: boolean; + vpn: { + result: boolean; + originTimezone: string; + originCountry: string; + methods: { + timezoneMismatch: boolean; + publicVPN: boolean; + auxiliaryMobile: boolean; + osMismatch: boolean; + }; + }; + proxy: boolean; + incognito: boolean; + tampering: { + result: boolean; + anomalyScore: number; + }; + clonedApp: boolean; + factoryReset: { + time: string; + timestamp: number; + }; + jailbroken: boolean; + frida: boolean; + privacySettings: boolean; + virtualMachine: boolean; + rawDeviceAttributes: { + 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: boolean; + locationSpoofing: boolean; + remoteControl: boolean; + } + | { + valid: false; + } + | 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 = { + getInfoFromRequest: (input: { + tenantId: string; + request: BaseRequest; + userContext: UserContext; + }) => InfoFromRequestHeaders; + + // this function will return hasProvidedV2SecretKey || hasProvidedV1SecretKey by default. + shouldEnforceGoogleRecaptchaTokenPresentInRequest: (input: { + tenantId: string; + actionType: SecurityChecksActionTypes; + userContext: UserContext; + }) => Promise; + + performGoogleRecaptchaV2: (input: { + tenantId: string; + infoFromRequest: InfoFromRequestHeaders; + googleRecaptchaToken: string; + userContext: UserContext; + }) => Promise; + + performGoogleRecaptchaV1: (input: { + tenantId: string; + infoFromRequest: InfoFromRequestHeaders; + googleRecaptchaToken: string; + userContext: UserContext; + }) => Promise; + + // this will return true if securityServiceApiKey is present in the config. + shouldEnforceSecurityServiceRequestIdPresentInRequest: (input: { + tenantId: string; + actionType: SecurityChecksActionTypes; + 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; + password?: string; // to check against breached password + securityServiceRequestId?: string; + email?: string; + phoneNumber?: string; + bruteForce?: { + key: string; + maxRequests: { + limit: number; + perTimeIntervalMS: number; + }[]; + }[]; + actionType?: SecurityChecksActionTypes; + 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; + rejectBasedOnImpossibleTravel?: boolean; + rejectBasedOnNewDevice?: boolean; + rejectBasedOnNumberOfUniqueDevicesForUser?: 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. + + // Note that for passwordless, we will call this during createCode. + getRateLimitForEmailPasswordSignIn: (input: { + tenantId: string; + session?: SessionContainer; + email: string; + infoFromRequest: InfoFromRequestHeaders; + userContext: UserContext; + }) => + | { + key: string; + 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: InfoFromRequestHeaders; + userContext: UserContext; + }) => + | { + key: string; + 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: InfoFromRequestHeaders; + userContext: UserContext; + }) => + | { + key: string; + maxRequests: { + limit: number; + perTimeIntervalMS: number; + }[]; + }[] + | undefined; + getRateLimitForSendingPasswordlessEmail: (input: { + tenantId: string; + session?: SessionContainer; + email: string; + infoFromRequest: InfoFromRequestHeaders; + userContext: UserContext; + }) => + | { + key: string; + maxRequests: { + limit: number; + perTimeIntervalMS: number; + }[]; + }[] + | undefined; + getRateLimitForSendingPasswordlessSms: (input: { + tenantId: string; + session?: SessionContainer; + phoneNumber: string; + infoFromRequest: InfoFromRequestHeaders; + userContext: UserContext; + }) => + | { + key: string; + maxRequests: { + limit: number; + perTimeIntervalMS: number; + }[]; + }[] + | undefined; + getRateLimitForResetPassword: (input: { + tenantId: string; + email: string; + infoFromRequest: InfoFromRequestHeaders; + userContext: UserContext; + }) => + | { + key: string; + 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: InfoFromRequestHeaders; + userContext: UserContext; + }) => + | { + key: string; + maxRequests: { + limit: number; + perTimeIntervalMS: number; + }[]; + }[] + | undefined; + + getRateLimitForTotpDeviceVerify: (input: { + tenantId: string; + session: SessionContainer; + deviceName: string; + infoFromRequest: InfoFromRequestHeaders; + userContext: UserContext; + }) => + | { + key: string; + maxRequests: { + limit: number; + perTimeIntervalMS: number; + }[]; + }[] + | undefined; + + getRateLimitForTotpVerify: (input: { + tenantId: string; + session: SessionContainer; + deviceName: string; + infoFromRequest: InfoFromRequestHeaders; + userContext: UserContext; + }) => + | { + key: string; + maxRequests: { + limit: number; + perTimeIntervalMS: number; + }[]; + }[] + | 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; + phoneNumber?: string; + userContext: UserContext; + }) => 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; + phoneNumber?: string; + userContext: UserContext; + }) => Promise<{ + userIdBanned?: boolean; + ipAddressBanned?: boolean; + emailBanned?: boolean; + phoneNumberBanned?: boolean; + }>; + + 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; + phoneNumber?: string; + userContext: UserContext; + }) => Promise; }; export type NetworkInterceptor = (request: HttpRequest, userContext: UserContext) => HttpRequest;