From 520a3202ccb730d85545a74305250d82ac0c519e Mon Sep 17 00:00:00 2001 From: Sattvik Chakravarthy Date: Fri, 15 Dec 2023 18:56:41 +0530 Subject: [PATCH] fix: MFA implementation (#743) * fix: type fix and account linking functions * fix: cdi version update * fix: more type updates * fix: tests * fix: totp recipe * fix: totp types * fix: update types * fix: totp apis * fix: user identifier info * fix: recipe tests * fix: test * fix: basic mfa impl * fix: pr comments * fix: tests * fix: factors setup from other recipe * fix: getFactorsSetupForUser impl * fix: getMFARequirementsForAuth impl * fix: isAllowedToSetupFactor impl * fix: addToDefaultRequiredFactorsForUser and getDefaultRequiredFactorsForUser impl * fix: typo * fix: build next array * fix: remove error file * fix: factorSetupForUser refactor * fix: next array * fix: api impl * fix: typo * fix: isValidFirstFactorForTenant * fix: impl * fix: updated impl * feat: fix and update mfa imlp to make all e2e tests pass * fix: adds overwriteSessionDuringSignIn config in session * fix: error messages in claims * fix: cleanup * fix: new errors for sign in up APIs * fix: add error in totp * fix: marked MFA TODOs * fix: new param in createNewSession * fix: impl cleanup * fix: remove MFA_ERROR * fix: cdi version * fix: test fix * fix: update/fix mfa impl to match e2e tests * fix: pr comments * fix: session user deleted error * fix: adding cache to getUserById * fix: get user cache * caching in querier * fix: mfa impl * fix: email selection * fix: mfa claims * fix: remove unnecessary file * fix: pr comment * fix: PR comments * fix: session handling * fix: review comments * fix: defaultRequiredFactorsForUser is now appwide * fix: using accountlinking instead of mfa for primary user and link accounts * fix: overwrite session flag refactor * fix: race conditions in createOrUpdateSessionForMultifactorAuthAfterFactorCompletion * fix: race conditions in createOrUpdateSessionForMultifactorAuthAfterFactorCompletion * fix: recipe functions refactor * fix: contact support case * fix: unnecessary file * fix: test * refactor: added shouldRefetch + fetchValue building the next array into MFAclaim (#758) * fix: usercontext type * fix: test * fix: test * feat: add access token payload param to claim.build * feat: expose addToDefaultRequiredFactorsForUser and remove tenantId param * fix: remaining TODOs * fix: auto init tests related to mfa * fix: recipe function tests * fix: create new session refactor * fix: recipe interface refactor * fix: userContext type fix * fix: test * fix: test * fix: session * fix: user context and support codes * fix: type fixes after merge * fix: test * fix: pr comments * fix: pr comment * fix: test * fix: available factors * fix: updated user object * fix: shouldAttemptAccountLinkingIfAllowed * fix: missed types and test fixes * fix: mfa fixes and tests * fix: more tests --------- Co-authored-by: Mihaly Lengyel --- coreDriverInterfaceSupported.json | 2 +- lib/build/framework/utils.js | 10 +- lib/build/index.d.ts | 30 +- lib/build/index.js | 39 +- .../emaildelivery/services/smtp.d.ts | 5 +- .../ingredients/emaildelivery/types.d.ts | 3 +- .../smsdelivery/services/twilio.d.ts | 5 +- lib/build/ingredients/smsdelivery/types.d.ts | 3 +- lib/build/querier.d.ts | 17 +- lib/build/querier.js | 35 +- lib/build/recipe/accountlinking/index.d.ts | 24 +- lib/build/recipe/accountlinking/index.js | 37 +- lib/build/recipe/accountlinking/recipe.d.ts | 22 +- lib/build/recipe/accountlinking/recipe.js | 65 +- .../accountlinking/recipeImplementation.js | 3 +- lib/build/recipe/accountlinking/types.d.ts | 28 +- lib/build/recipe/accountlinking/utils.d.ts | 9 +- lib/build/recipe/accountlinking/utils.js | 71 +- lib/build/recipe/dashboard/api/analytics.d.ts | 3 +- lib/build/recipe/dashboard/api/analytics.js | 6 +- .../recipe/dashboard/api/apiKeyProtector.d.ts | 3 +- .../recipe/dashboard/api/apiKeyProtector.js | 14 - lib/build/recipe/dashboard/api/dashboard.d.ts | 3 +- .../recipe/dashboard/api/listTenants.d.ts | 8 +- .../recipe/dashboard/api/search/tagsGet.d.ts | 3 +- lib/build/recipe/dashboard/api/signIn.d.ts | 3 +- lib/build/recipe/dashboard/api/signOut.d.ts | 8 +- .../api/userdetails/userEmailVerifyPut.d.ts | 3 +- .../api/userdetails/userEmailVerifyPut.js | 1 + .../userdetails/userEmailVerifyTokenPost.d.ts | 3 +- .../api/userdetails/userMetadataPut.d.ts | 3 +- .../api/userdetails/userPasswordPut.d.ts | 3 +- .../dashboard/api/userdetails/userPut.d.ts | 3 +- .../dashboard/api/userdetails/userPut.js | 6 +- .../api/userdetails/userSessionsPost.d.ts | 3 +- .../api/userdetails/userUnlinkGet.d.ts | 3 +- .../recipe/dashboard/api/usersCountGet.d.ts | 8 +- .../recipe/dashboard/api/usersCountGet.js | 4 +- lib/build/recipe/dashboard/api/usersGet.d.ts | 3 +- .../recipe/dashboard/api/validateKey.d.ts | 3 +- lib/build/recipe/dashboard/recipe.d.ts | 4 +- lib/build/recipe/dashboard/types.d.ts | 14 +- lib/build/recipe/dashboard/utils.d.ts | 6 +- lib/build/recipe/dashboard/utils.js | 8 +- .../recipe/emailpassword/api/emailExists.d.ts | 3 +- .../api/generatePasswordResetToken.d.ts | 3 +- .../emailpassword/api/implementation.js | 146 +++- .../emailpassword/api/passwordReset.d.ts | 3 +- .../recipe/emailpassword/api/signin.d.ts | 3 +- .../recipe/emailpassword/api/signup.d.ts | 3 +- .../services/backwardCompatibility/index.d.ts | 4 +- .../emaildelivery/services/smtp/index.d.ts | 3 +- lib/build/recipe/emailpassword/index.d.ts | 21 +- lib/build/recipe/emailpassword/index.js | 29 +- lib/build/recipe/emailpassword/recipe.d.ts | 4 +- lib/build/recipe/emailpassword/recipe.js | 30 + .../emailpassword/recipeImplementation.js | 21 +- lib/build/recipe/emailpassword/types.d.ts | 31 +- lib/build/recipe/emailpassword/utils.d.ts | 4 +- .../emailverification/api/emailVerify.d.ts | 3 +- .../api/generateEmailVerifyToken.d.ts | 3 +- .../emailVerificationClaim.js | 2 +- .../services/backwardCompatibility/index.d.ts | 4 +- .../emaildelivery/services/smtp/index.d.ts | 3 +- lib/build/recipe/emailverification/index.d.ts | 20 +- lib/build/recipe/emailverification/index.js | 60 +- .../recipe/emailverification/recipe.d.ts | 8 +- lib/build/recipe/emailverification/types.d.ts | 24 +- lib/build/recipe/emailverification/utils.d.ts | 4 +- lib/build/recipe/jwt/api/getJWKS.d.ts | 3 +- lib/build/recipe/jwt/index.d.ts | 4 +- lib/build/recipe/jwt/index.js | 5 +- lib/build/recipe/jwt/recipe.d.ts | 6 +- lib/build/recipe/jwt/recipe.js | 2 +- lib/build/recipe/jwt/recipeImplementation.js | 2 +- lib/build/recipe/jwt/types.d.ts | 8 +- .../multifactorauth/api/implementation.js | 121 ++- .../recipe/multifactorauth/api/mfaInfo.d.ts | 3 +- .../recipe/multifactorauth/api/mfaInfo.js | 11 +- .../recipe/multifactorauth/constants.d.ts | 2 +- lib/build/recipe/multifactorauth/constants.js | 2 +- lib/build/recipe/multifactorauth/error.d.ts | 5 - lib/build/recipe/multifactorauth/error.js | 29 - lib/build/recipe/multifactorauth/index.d.ts | 18 +- lib/build/recipe/multifactorauth/index.js | 75 +- .../multifactorauth/multiFactorAuthClaim.d.ts | 23 +- .../multifactorauth/multiFactorAuthClaim.js | 191 ++++- lib/build/recipe/multifactorauth/recipe.d.ts | 86 +- lib/build/recipe/multifactorauth/recipe.js | 360 ++++++++- .../multifactorauth/recipeImplementation.d.ts | 4 +- .../multifactorauth/recipeImplementation.js | 234 ++++-- lib/build/recipe/multifactorauth/types.d.ts | 94 +-- lib/build/recipe/multifactorauth/utils.d.ts | 10 +- lib/build/recipe/multifactorauth/utils.js | 10 +- .../multitenancy/allowedDomainsClaim.js | 2 +- .../recipe/multitenancy/api/loginMethods.d.ts | 3 +- lib/build/recipe/multitenancy/index.d.ts | 55 +- lib/build/recipe/multitenancy/index.js | 17 +- lib/build/recipe/multitenancy/recipe.d.ts | 6 +- lib/build/recipe/multitenancy/types.d.ts | 83 +- .../api/getOpenIdDiscoveryConfiguration.d.ts | 3 +- .../api/getOpenIdDiscoveryConfiguration.js | 14 - lib/build/recipe/openid/index.d.ts | 6 +- lib/build/recipe/openid/index.js | 7 +- lib/build/recipe/openid/recipe.d.ts | 11 +- lib/build/recipe/openid/recipe.js | 4 +- lib/build/recipe/openid/types.d.ts | 10 +- .../recipe/passwordless/api/consumeCode.d.ts | 3 +- .../recipe/passwordless/api/createCode.d.ts | 3 +- .../recipe/passwordless/api/emailExists.d.ts | 3 +- .../recipe/passwordless/api/implementation.js | 111 ++- .../passwordless/api/phoneNumberExists.d.ts | 3 +- .../recipe/passwordless/api/resendCode.d.ts | 3 +- .../services/backwardCompatibility/index.d.ts | 4 +- .../emaildelivery/services/smtp/index.d.ts | 3 +- lib/build/recipe/passwordless/index.d.ts | 42 +- lib/build/recipe/passwordless/index.js | 143 ++-- lib/build/recipe/passwordless/recipe.d.ts | 15 +- lib/build/recipe/passwordless/recipe.js | 53 +- .../passwordless/recipeImplementation.js | 16 +- .../services/backwardCompatibility/index.d.ts | 3 +- .../smsdelivery/services/twilio/index.d.ts | 3 +- lib/build/recipe/passwordless/types.d.ts | 47 +- lib/build/recipe/passwordless/utils.d.ts | 2 + lib/build/recipe/passwordless/utils.js | 24 +- lib/build/recipe/session/api/refresh.d.ts | 3 +- lib/build/recipe/session/api/signout.d.ts | 3 +- .../claimBaseClasses/primitiveArrayClaim.d.ts | 15 +- .../claimBaseClasses/primitiveClaim.d.ts | 15 +- .../recipe/session/cookieAndHeaders.d.ts | 9 +- lib/build/recipe/session/index.d.ts | 76 +- lib/build/recipe/session/index.js | 140 ++-- lib/build/recipe/session/recipe.d.ts | 13 +- lib/build/recipe/session/recipe.js | 16 +- .../recipe/session/recipeImplementation.js | 2 + lib/build/recipe/session/sessionClass.d.ts | 32 +- lib/build/recipe/session/sessionClass.js | 72 +- .../recipe/session/sessionFunctions.d.ts | 25 +- lib/build/recipe/session/sessionFunctions.js | 4 +- .../session/sessionRequestFunctions.d.ts | 8 +- .../recipe/session/sessionRequestFunctions.js | 2 +- lib/build/recipe/session/types.d.ts | 137 ++-- lib/build/recipe/session/types.js | 4 +- lib/build/recipe/session/utils.d.ts | 20 +- lib/build/recipe/session/utils.js | 34 +- .../recipe/thirdparty/api/appleRedirect.d.ts | 3 +- .../thirdparty/api/authorisationUrl.d.ts | 3 +- .../recipe/thirdparty/api/implementation.js | 102 ++- lib/build/recipe/thirdparty/api/signinup.d.ts | 3 +- lib/build/recipe/thirdparty/index.d.ts | 6 +- lib/build/recipe/thirdparty/index.js | 11 +- .../thirdparty/providers/configUtils.d.ts | 3 +- lib/build/recipe/thirdparty/recipe.d.ts | 4 +- lib/build/recipe/thirdparty/recipe.js | 29 + .../recipe/thirdparty/recipeImplementation.js | 21 +- lib/build/recipe/thirdparty/types.d.ts | 41 +- .../services/backwardCompatibility/index.d.ts | 4 +- .../emaildelivery/services/smtp/index.d.ts | 3 +- .../recipe/thirdpartyemailpassword/index.d.ts | 26 +- .../recipe/thirdpartyemailpassword/index.js | 42 +- .../thirdpartyemailpassword/recipe.d.ts | 4 +- .../thirdPartyRecipeImplementation.js | 1 - .../recipe/thirdpartyemailpassword/types.d.ts | 47 +- .../services/backwardCompatibility/index.d.ts | 4 +- .../emaildelivery/services/smtp/index.d.ts | 3 +- .../recipe/thirdpartypasswordless/index.d.ts | 46 +- .../recipe/thirdpartypasswordless/index.js | 160 ++-- .../recipe/thirdpartypasswordless/recipe.d.ts | 4 +- .../thirdPartyRecipeImplementation.js | 1 - .../services/backwardCompatibility/index.d.ts | 3 +- .../services/supertokens/index.d.ts | 3 +- .../smsdelivery/services/twilio/index.d.ts | 3 +- .../recipe/thirdpartypasswordless/types.d.ts | 64 +- lib/build/recipe/totp/api/createDevice.d.ts | 3 +- lib/build/recipe/totp/api/implementation.js | 57 +- lib/build/recipe/totp/api/listDevices.d.ts | 3 +- lib/build/recipe/totp/api/removeDevice.d.ts | 3 +- lib/build/recipe/totp/api/verifyDevice.d.ts | 3 +- lib/build/recipe/totp/api/verifyTOTP.d.ts | 3 +- lib/build/recipe/totp/index.d.ts | 12 +- lib/build/recipe/totp/index.js | 13 +- lib/build/recipe/totp/recipe.d.ts | 4 +- lib/build/recipe/totp/recipe.js | 38 + lib/build/recipe/totp/recipeImplementation.js | 6 - lib/build/recipe/totp/types.d.ts | 32 +- lib/build/recipe/usermetadata/index.d.ts | 6 +- lib/build/recipe/usermetadata/index.js | 7 +- .../usermetadata/recipeImplementation.js | 48 +- lib/build/recipe/usermetadata/types.d.ts | 10 +- lib/build/recipe/userroles/index.d.ts | 20 +- lib/build/recipe/userroles/index.js | 21 +- lib/build/recipe/userroles/permissionClaim.js | 2 +- .../recipe/userroles/recipeImplementation.js | 2 +- lib/build/recipe/userroles/types.d.ts | 21 +- lib/build/recipe/userroles/userRoleClaim.js | 2 +- lib/build/recipeModule.d.ts | 13 +- lib/build/supertokens.d.ts | 24 +- lib/build/supertokens.js | 21 +- lib/build/types.d.ts | 15 +- lib/build/utils.d.ts | 10 +- lib/build/utils.js | 6 +- lib/build/version.js | 2 +- lib/ts/framework/utils.ts | 4 +- lib/ts/index.ts | 65 +- .../emaildelivery/services/smtp.ts | 5 +- lib/ts/ingredients/emaildelivery/types.ts | 3 +- .../smsdelivery/services/twilio.ts | 5 +- lib/ts/ingredients/smsdelivery/types.ts | 3 +- lib/ts/querier.ts | 52 +- lib/ts/recipe/accountlinking/index.ts | 41 +- lib/ts/recipe/accountlinking/recipe.ts | 89 +-- .../accountlinking/recipeImplementation.ts | 31 +- lib/ts/recipe/accountlinking/types.ts | 28 +- lib/ts/recipe/accountlinking/utils.ts | 74 +- lib/ts/recipe/dashboard/api/analytics.ts | 7 +- .../recipe/dashboard/api/apiKeyProtector.ts | 3 +- lib/ts/recipe/dashboard/api/dashboard.ts | 3 +- lib/ts/recipe/dashboard/api/listTenants.ts | 3 +- lib/ts/recipe/dashboard/api/search/tagsGet.ts | 3 +- lib/ts/recipe/dashboard/api/signIn.ts | 3 +- lib/ts/recipe/dashboard/api/signOut.ts | 3 +- .../api/userdetails/userEmailVerifyGet.ts | 3 +- .../api/userdetails/userEmailVerifyPut.ts | 4 +- .../userdetails/userEmailVerifyTokenPost.ts | 3 +- .../dashboard/api/userdetails/userGet.ts | 4 +- .../api/userdetails/userMetadataGet.ts | 3 +- .../api/userdetails/userMetadataPut.ts | 3 +- .../api/userdetails/userPasswordPut.ts | 3 +- .../dashboard/api/userdetails/userPut.ts | 9 +- .../api/userdetails/userSessionsGet.ts | 3 +- .../api/userdetails/userSessionsPost.ts | 3 +- .../api/userdetails/userUnlinkGet.ts | 3 +- lib/ts/recipe/dashboard/api/usersCountGet.ts | 5 +- lib/ts/recipe/dashboard/api/usersGet.ts | 3 +- lib/ts/recipe/dashboard/api/validateKey.ts | 7 +- lib/ts/recipe/dashboard/recipe.ts | 4 +- lib/ts/recipe/dashboard/types.ts | 14 +- lib/ts/recipe/dashboard/utils.ts | 18 +- .../recipe/emailpassword/api/emailExists.ts | 3 +- .../api/generatePasswordResetToken.ts | 3 +- .../emailpassword/api/implementation.ts | 177 ++++- .../recipe/emailpassword/api/passwordReset.ts | 3 +- lib/ts/recipe/emailpassword/api/signin.ts | 3 +- lib/ts/recipe/emailpassword/api/signup.ts | 3 +- .../services/backwardCompatibility/index.ts | 4 +- .../emaildelivery/services/smtp/index.ts | 3 +- .../smtp/serviceImplementation/index.ts | 3 +- lib/ts/recipe/emailpassword/index.ts | 55 +- lib/ts/recipe/emailpassword/recipe.ts | 41 +- .../emailpassword/recipeImplementation.ts | 35 +- lib/ts/recipe/emailpassword/types.ts | 35 +- lib/ts/recipe/emailpassword/utils.ts | 4 +- .../emailverification/api/emailVerify.ts | 3 +- .../api/generateEmailVerifyToken.ts | 3 +- .../emailVerificationClaim.ts | 2 +- .../services/backwardCompatibility/index.ts | 4 +- .../emaildelivery/services/smtp/index.ts | 3 +- .../services/smtp/serviceImplementation.ts | 3 +- lib/ts/recipe/emailverification/index.ts | 54 +- lib/ts/recipe/emailverification/recipe.ts | 8 +- .../emailverification/recipeImplementation.ts | 11 +- lib/ts/recipe/emailverification/types.ts | 28 +- lib/ts/recipe/emailverification/utils.ts | 4 +- lib/ts/recipe/jwt/api/getJWKS.ts | 3 +- lib/ts/recipe/jwt/api/implementation.ts | 4 +- lib/ts/recipe/jwt/index.ts | 14 +- lib/ts/recipe/jwt/recipe.ts | 6 +- lib/ts/recipe/jwt/recipeImplementation.ts | 6 +- lib/ts/recipe/jwt/types.ts | 8 +- .../multifactorauth/api/implementation.ts | 92 ++- lib/ts/recipe/multifactorauth/api/mfaInfo.ts | 16 +- lib/ts/recipe/multifactorauth/constants.ts | 2 +- lib/ts/recipe/multifactorauth/error.ts | 25 - lib/ts/recipe/multifactorauth/index.ts | 80 +- .../multifactorauth/multiFactorAuthClaim.ts | 204 ++++- lib/ts/recipe/multifactorauth/recipe.ts | 388 ++++++++- .../multifactorauth/recipeImplementation.ts | 279 +++---- lib/ts/recipe/multifactorauth/types.ts | 95 +-- lib/ts/recipe/multifactorauth/utils.ts | 10 +- .../multitenancy/allowedDomainsClaim.ts | 2 +- .../recipe/multitenancy/api/loginMethods.ts | 3 +- lib/ts/recipe/multitenancy/index.ts | 68 +- lib/ts/recipe/multitenancy/recipe.ts | 6 +- lib/ts/recipe/multitenancy/types.ts | 78 +- .../api/getOpenIdDiscoveryConfiguration.ts | 3 +- lib/ts/recipe/openid/api/implementation.ts | 4 +- lib/ts/recipe/openid/index.ts | 18 +- lib/ts/recipe/openid/recipe.ts | 13 +- lib/ts/recipe/openid/recipeImplementation.ts | 3 +- lib/ts/recipe/openid/types.ts | 10 +- lib/ts/recipe/passwordless/api/consumeCode.ts | 3 +- lib/ts/recipe/passwordless/api/createCode.ts | 3 +- lib/ts/recipe/passwordless/api/emailExists.ts | 3 +- .../recipe/passwordless/api/implementation.ts | 122 ++- .../passwordless/api/phoneNumberExists.ts | 3 +- lib/ts/recipe/passwordless/api/resendCode.ts | 3 +- .../services/backwardCompatibility/index.ts | 4 +- .../emaildelivery/services/smtp/index.ts | 3 +- .../services/smtp/serviceImplementation.ts | 3 +- lib/ts/recipe/passwordless/index.ts | 79 +- lib/ts/recipe/passwordless/recipe.ts | 78 +- .../passwordless/recipeImplementation.ts | 16 +- .../services/backwardCompatibility/index.ts | 3 +- .../smsdelivery/services/twilio/index.ts | 3 +- .../services/twilio/serviceImplementation.ts | 3 +- lib/ts/recipe/passwordless/types.ts | 47 +- lib/ts/recipe/passwordless/utils.ts | 24 + lib/ts/recipe/session/api/implementation.ts | 8 +- lib/ts/recipe/session/api/refresh.ts | 3 +- lib/ts/recipe/session/api/signout.ts | 3 +- .../claimBaseClasses/primitiveArrayClaim.ts | 15 +- .../claimBaseClasses/primitiveClaim.ts | 15 +- lib/ts/recipe/session/cookieAndHeaders.ts | 9 +- lib/ts/recipe/session/index.ts | 174 +++-- lib/ts/recipe/session/recipe.ts | 19 +- lib/ts/recipe/session/recipeImplementation.ts | 41 +- lib/ts/recipe/session/sessionClass.ts | 106 +-- lib/ts/recipe/session/sessionFunctions.ts | 29 +- .../recipe/session/sessionRequestFunctions.ts | 10 +- lib/ts/recipe/session/types.ts | 140 ++-- lib/ts/recipe/session/utils.ts | 62 +- lib/ts/recipe/thirdparty/api/appleRedirect.ts | 3 +- .../recipe/thirdparty/api/authorisationUrl.ts | 3 +- .../recipe/thirdparty/api/implementation.ts | 114 ++- lib/ts/recipe/thirdparty/api/signinup.ts | 3 +- lib/ts/recipe/thirdparty/index.ts | 11 +- .../thirdparty/providers/configUtils.ts | 5 +- lib/ts/recipe/thirdparty/recipe.ts | 39 +- .../recipe/thirdparty/recipeImplementation.ts | 27 +- lib/ts/recipe/thirdparty/types.ts | 41 +- .../services/backwardCompatibility/index.ts | 4 +- .../emaildelivery/services/smtp/index.ts | 3 +- .../recipe/thirdpartyemailpassword/index.ts | 56 +- .../recipe/thirdpartyemailpassword/recipe.ts | 4 +- .../emailPasswordRecipeImplementation.ts | 28 +- .../recipeImplementation/index.ts | 39 +- .../thirdPartyRecipeImplementation.ts | 13 +- .../recipe/thirdpartyemailpassword/types.ts | 47 +- .../services/backwardCompatibility/index.ts | 4 +- .../emaildelivery/services/smtp/index.ts | 3 +- .../smtp/serviceImplementation/index.ts | 3 +- .../passwordlessServiceImplementation.ts | 3 +- lib/ts/recipe/thirdpartypasswordless/index.ts | 86 +- .../recipe/thirdpartypasswordless/recipe.ts | 4 +- .../recipeImplementation/index.ts | 11 +- .../thirdPartyRecipeImplementation.ts | 13 +- .../services/backwardCompatibility/index.ts | 3 +- .../smsdelivery/services/supertokens/index.ts | 3 +- .../smsdelivery/services/twilio/index.ts | 3 +- lib/ts/recipe/thirdpartypasswordless/types.ts | 64 +- lib/ts/recipe/totp/api/createDevice.ts | 3 +- lib/ts/recipe/totp/api/implementation.ts | 68 +- lib/ts/recipe/totp/api/listDevices.ts | 3 +- lib/ts/recipe/totp/api/removeDevice.ts | 3 +- lib/ts/recipe/totp/api/verifyDevice.ts | 3 +- lib/ts/recipe/totp/api/verifyTOTP.ts | 3 +- lib/ts/recipe/totp/index.ts | 42 +- lib/ts/recipe/totp/recipe.ts | 46 +- lib/ts/recipe/totp/recipeImplementation.ts | 19 +- lib/ts/recipe/totp/types.ts | 32 +- lib/ts/recipe/usermetadata/index.ts | 13 +- .../usermetadata/recipeImplementation.ts | 12 +- lib/ts/recipe/usermetadata/types.ts | 10 +- lib/ts/recipe/userroles/index.ts | 41 +- lib/ts/recipe/userroles/permissionClaim.ts | 2 +- .../recipe/userroles/recipeImplementation.ts | 2 +- lib/ts/recipe/userroles/types.ts | 21 +- lib/ts/recipe/userroles/userRoleClaim.ts | 2 +- lib/ts/recipeModule.ts | 13 +- lib/ts/supertokens.ts | 46 +- lib/ts/types.ts | 14 +- lib/ts/utils.ts | 18 +- lib/ts/version.ts | 2 +- test/accountlinking/emailpassword.test.js | 5 +- test/accountlinking/emailpasswordapis.test.js | 4 +- .../accountlinking/emailpasswordapis2.test.js | 5 +- test/accountlinking/helperFunctions.test.js | 34 +- test/accountlinking/thirdpartyapis.test.js | 42 +- test/accountlinking/userstructure.test.js | 1 + .../repeatedResponseHeader.test.js | 4 +- .../crossframework/unauthorised.test.js | 2 +- test/jwt/getJWKS.test.js | 4 +- test/jwt/override.test.js | 4 +- test/mfa/mfa.api.test.js | 320 +++++++- test/mfa/mfa.autoInit.test.js | 59 ++ test/mfa/mfa.claims.test.js | 0 test/mfa/mfa.recipeFunctions.test.js | 300 +++++++ test/mfa/mfa.withAccountLinking.test.js | 738 ++++++++++++++++++ test/mfa/utils.js | 280 +++++++ test/querier.test.js | 20 +- test/ratelimiting.test.js | 12 +- test/session.test.js | 318 +++++--- test/session/accessTokenVersions.test.js | 25 +- .../overwriteSessionDuringSignIn.test.js | 313 ++++++++ ...sessionAccessTokenSigningKeyUpdate.test.js | 57 +- test/totp/recipeFunctions.test.js | 2 +- test/userContext.test.js | 2 +- test/utils.js | 2 + test/with-typescript/index.ts | 43 +- 399 files changed, 8666 insertions(+), 3141 deletions(-) delete mode 100644 lib/build/recipe/multifactorauth/error.d.ts delete mode 100644 lib/build/recipe/multifactorauth/error.js delete mode 100644 lib/ts/recipe/multifactorauth/error.ts create mode 100644 test/mfa/mfa.autoInit.test.js delete mode 100644 test/mfa/mfa.claims.test.js create mode 100644 test/mfa/mfa.recipeFunctions.test.js create mode 100644 test/mfa/mfa.withAccountLinking.test.js create mode 100644 test/mfa/utils.js create mode 100644 test/session/overwriteSessionDuringSignIn.test.js diff --git a/coreDriverInterfaceSupported.json b/coreDriverInterfaceSupported.json index f4143a959..e389b4b5b 100644 --- a/coreDriverInterfaceSupported.json +++ b/coreDriverInterfaceSupported.json @@ -1,4 +1,4 @@ { "_comment": "contains a list of core-driver interfaces branch names that this core supports", - "versions": ["4.1"] + "versions": ["5.0"] } diff --git a/lib/build/framework/utils.js b/lib/build/framework/utils.js index 76803c26c..6607ae65f 100644 --- a/lib/build/framework/utils.js +++ b/lib/build/framework/utils.js @@ -206,6 +206,7 @@ async function assertFormDataBodyParserHasBeenUsedForExpressLikeRequest(request) } exports.assertFormDataBodyParserHasBeenUsedForExpressLikeRequest = assertFormDataBodyParserHasBeenUsedForExpressLikeRequest; function setHeaderForExpressLikeResponse(res, key, value, allowDuplicateKey) { + var _a; try { let existingHeaders = res.getHeaders(); let existingValue = existingHeaders[key.toLowerCase()]; @@ -238,7 +239,14 @@ function setHeaderForExpressLikeResponse(res, key, value, allowDuplicateKey) { } } } catch (err) { - throw new Error("Error while setting header with key: " + key + " and value: " + value); + throw new Error( + "Error while setting header with key: " + + key + + " and value: " + + value + + "\nError: " + + ((_a = err.message) !== null && _a !== void 0 ? _a : err) + ); } } exports.setHeaderForExpressLikeResponse = setHeaderForExpressLikeResponse; diff --git a/lib/build/index.d.ts b/lib/build/index.d.ts index 179c163ce..2bbc7017d 100644 --- a/lib/build/index.d.ts +++ b/lib/build/index.d.ts @@ -1,7 +1,7 @@ // @ts-nocheck import SuperTokens from "./supertokens"; import SuperTokensError from "./error"; -import { User as UserType } from "./types"; +import { UserContext, User as UserType } from "./types"; import { AccountInfo } from "./recipe/accountlinking/types"; import RecipeUserId from "./recipeUserId"; import { User } from "./user"; @@ -11,7 +11,11 @@ export default class SuperTokensWrapper { static RecipeUserId: typeof RecipeUserId; static User: typeof User; static getAllCORSHeaders(): string[]; - static getUserCount(includeRecipeIds?: string[], tenantId?: string, userContext?: any): Promise; + static getUserCount( + includeRecipeIds?: string[], + tenantId?: string, + userContext?: Record + ): Promise; static getUsersOldestFirst(input: { tenantId: string; limit?: number; @@ -20,7 +24,7 @@ export default class SuperTokensWrapper { query?: { [key: string]: string; }; - userContext?: any; + userContext?: Record; }): Promise<{ users: UserType[]; nextPaginationToken?: string; @@ -33,7 +37,7 @@ export default class SuperTokensWrapper { query?: { [key: string]: string; }; - userContext?: any; + userContext?: Record; }): Promise<{ users: UserType[]; nextPaginationToken?: string; @@ -43,7 +47,7 @@ export default class SuperTokensWrapper { externalUserId: string; externalUserIdInfo?: string; force?: boolean; - userContext?: any; + userContext?: Record; }): Promise< | { status: "OK" | "UNKNOWN_SUPERTOKENS_USER_ID_ERROR"; @@ -57,7 +61,7 @@ export default class SuperTokensWrapper { static getUserIdMapping(input: { userId: string; userIdType?: "SUPERTOKENS" | "EXTERNAL" | "ANY"; - userContext?: any; + userContext?: Record; }): Promise< | { status: "OK"; @@ -73,7 +77,7 @@ export default class SuperTokensWrapper { userId: string; userIdType?: "SUPERTOKENS" | "EXTERNAL" | "ANY"; force?: boolean; - userContext?: any; + userContext?: Record; }): Promise<{ status: "OK"; didMappingExist: boolean; @@ -82,26 +86,28 @@ export default class SuperTokensWrapper { userId: string; userIdType?: "SUPERTOKENS" | "EXTERNAL" | "ANY"; externalUserIdInfo?: string; - userContext?: any; + userContext?: Record; }): Promise<{ status: "OK" | "UNKNOWN_MAPPING_ERROR"; }>; - static getUser(userId: string, userContext?: any): Promise; + static getUser(userId: string, userContext?: Record): Promise; static listUsersByAccountInfo( tenantId: string, accountInfo: AccountInfo, doUnionOfAccountInfo?: boolean, - userContext?: any + userContext?: Record ): Promise; static deleteUser( userId: string, removeAllLinkedAccounts?: boolean, - userContext?: any + userContext?: Record ): Promise<{ status: "OK"; }>; static convertToRecipeUserId(recipeUserId: string): RecipeUserId; - static getRequestFromUserContext(userContext: any | undefined): import("./framework").BaseRequest | undefined; + static getRequestFromUserContext( + userContext: UserContext | undefined + ): import("./framework").BaseRequest | undefined; } export declare let init: typeof SuperTokens.init; export declare let getAllCORSHeaders: typeof SuperTokensWrapper.getAllCORSHeaders; diff --git a/lib/build/index.js b/lib/build/index.js index 4d4d88e22..ecdd1b8b6 100644 --- a/lib/build/index.js +++ b/lib/build/index.js @@ -25,44 +25,63 @@ const error_1 = __importDefault(require("./error")); const recipe_1 = __importDefault(require("./recipe/accountlinking/recipe")); const recipeUserId_1 = __importDefault(require("./recipeUserId")); const user_1 = require("./user"); +const utils_1 = require("./utils"); // For Express class SuperTokensWrapper { static getAllCORSHeaders() { return supertokens_1.default.getInstanceOrThrowError().getAllCORSHeaders(); } static getUserCount(includeRecipeIds, tenantId, userContext) { - return supertokens_1.default.getInstanceOrThrowError().getUserCount(includeRecipeIds, tenantId, userContext); + return supertokens_1.default + .getInstanceOrThrowError() + .getUserCount(includeRecipeIds, tenantId, utils_1.getUserContext(userContext)); } static getUsersOldestFirst(input) { return recipe_1.default.getInstance().recipeInterfaceImpl.getUsers( Object.assign(Object.assign({ timeJoinedOrder: "ASC" }, input), { - userContext: input.userContext === undefined ? {} : input.userContext, + userContext: utils_1.getUserContext(input.userContext), }) ); } static getUsersNewestFirst(input) { return recipe_1.default.getInstance().recipeInterfaceImpl.getUsers( Object.assign(Object.assign({ timeJoinedOrder: "DESC" }, input), { - userContext: input.userContext === undefined ? {} : input.userContext, + userContext: utils_1.getUserContext(input.userContext), }) ); } static createUserIdMapping(input) { - return supertokens_1.default.getInstanceOrThrowError().createUserIdMapping(input); + return supertokens_1.default + .getInstanceOrThrowError() + .createUserIdMapping( + Object.assign(Object.assign({}, input), { userContext: utils_1.getUserContext(input.userContext) }) + ); } static getUserIdMapping(input) { - return supertokens_1.default.getInstanceOrThrowError().getUserIdMapping(input); + return supertokens_1.default + .getInstanceOrThrowError() + .getUserIdMapping( + Object.assign(Object.assign({}, input), { userContext: utils_1.getUserContext(input.userContext) }) + ); } static deleteUserIdMapping(input) { - return supertokens_1.default.getInstanceOrThrowError().deleteUserIdMapping(input); + return supertokens_1.default + .getInstanceOrThrowError() + .deleteUserIdMapping( + Object.assign(Object.assign({}, input), { userContext: utils_1.getUserContext(input.userContext) }) + ); } static updateOrDeleteUserIdMappingInfo(input) { - return supertokens_1.default.getInstanceOrThrowError().updateOrDeleteUserIdMappingInfo(input); + return supertokens_1.default + .getInstanceOrThrowError() + .updateOrDeleteUserIdMappingInfo( + Object.assign(Object.assign({}, input), { userContext: utils_1.getUserContext(input.userContext) }) + ); } static async getUser(userId, userContext) { return await recipe_1.default.getInstance().recipeInterfaceImpl.getUser({ userId, - userContext: userContext === undefined ? {} : userContext, + userContext: utils_1.getUserContext(userContext), }); } static async listUsersByAccountInfo(tenantId, accountInfo, doUnionOfAccountInfo = false, userContext) { @@ -70,14 +89,14 @@ class SuperTokensWrapper { tenantId, accountInfo, doUnionOfAccountInfo, - userContext: userContext === undefined ? {} : userContext, + userContext: utils_1.getUserContext(userContext), }); } static async deleteUser(userId, removeAllLinkedAccounts = true, userContext) { return await recipe_1.default.getInstance().recipeInterfaceImpl.deleteUser({ userId, removeAllLinkedAccounts, - userContext: userContext === undefined ? {} : userContext, + userContext: utils_1.getUserContext(userContext), }); } static convertToRecipeUserId(recipeUserId) { diff --git a/lib/build/ingredients/emaildelivery/services/smtp.d.ts b/lib/build/ingredients/emaildelivery/services/smtp.d.ts index 43a3c5437..a74be5624 100644 --- a/lib/build/ingredients/emaildelivery/services/smtp.d.ts +++ b/lib/build/ingredients/emaildelivery/services/smtp.d.ts @@ -1,5 +1,6 @@ // @ts-nocheck import OverrideableBuilder from "supertokens-js-override"; +import { UserContext } from "../../../types"; export interface SMTPServiceConfig { host: string; from: { @@ -18,13 +19,13 @@ export interface GetContentResult { toEmail: string; } export declare type TypeInputSendRawEmail = GetContentResult & { - userContext: any; + userContext: UserContext; }; export declare type ServiceInterface = { sendRawEmail: (input: TypeInputSendRawEmail) => Promise; getContent: ( input: T & { - userContext: any; + userContext: UserContext; } ) => Promise; }; diff --git a/lib/build/ingredients/emaildelivery/types.d.ts b/lib/build/ingredients/emaildelivery/types.d.ts index d6f541c74..2986807f5 100644 --- a/lib/build/ingredients/emaildelivery/types.d.ts +++ b/lib/build/ingredients/emaildelivery/types.d.ts @@ -1,10 +1,11 @@ // @ts-nocheck import OverrideableBuilder from "supertokens-js-override"; +import { UserContext } from "../../types"; export declare type EmailDeliveryInterface = { sendEmail: ( input: T & { tenantId: string; - userContext: any; + userContext: UserContext; } ) => Promise; }; diff --git a/lib/build/ingredients/smsdelivery/services/twilio.d.ts b/lib/build/ingredients/smsdelivery/services/twilio.d.ts index d2d0dabae..03e22fb22 100644 --- a/lib/build/ingredients/smsdelivery/services/twilio.d.ts +++ b/lib/build/ingredients/smsdelivery/services/twilio.d.ts @@ -1,6 +1,7 @@ // @ts-nocheck import OverrideableBuilder from "supertokens-js-override"; import { ClientOpts } from "twilio/lib/base/BaseTwilio"; +import { UserContext } from "../../../types"; /** * only one of "from" and "messagingServiceSid" should be passed. * if both are passed, we should throw error to the user @@ -27,7 +28,7 @@ export interface GetContentResult { toPhoneNumber: string; } export declare type TypeInputSendRawSms = GetContentResult & { - userContext: any; + userContext: UserContext; } & ( | { from: string; @@ -40,7 +41,7 @@ export declare type ServiceInterface = { sendRawSms: (input: TypeInputSendRawSms) => Promise; getContent: ( input: T & { - userContext: any; + userContext: UserContext; } ) => Promise; }; diff --git a/lib/build/ingredients/smsdelivery/types.d.ts b/lib/build/ingredients/smsdelivery/types.d.ts index 0928ed5a4..781d1bfdf 100644 --- a/lib/build/ingredients/smsdelivery/types.d.ts +++ b/lib/build/ingredients/smsdelivery/types.d.ts @@ -1,10 +1,11 @@ // @ts-nocheck import OverrideableBuilder from "supertokens-js-override"; +import { UserContext } from "../../types"; export declare type SmsDeliveryInterface = { sendSms: ( input: T & { tenantId: string; - userContext: any; + userContext: UserContext; } ) => Promise; }; diff --git a/lib/build/querier.d.ts b/lib/build/querier.d.ts index 51ab36d5f..e8dff754e 100644 --- a/lib/build/querier.d.ts +++ b/lib/build/querier.d.ts @@ -1,6 +1,7 @@ // @ts-nocheck import NormalisedURLDomain from "./normalisedURLDomain"; import NormalisedURLPath from "./normalisedURLPath"; +import { UserContext } from "./types"; import { NetworkInterceptor } from "./types"; export declare class Querier { private static initCalled; @@ -25,22 +26,28 @@ export declare class Querier { apiKey?: string, networkInterceptor?: NetworkInterceptor ): void; - sendPostRequest: (path: NormalisedURLPath, body: any, userContext: any) => Promise; - sendDeleteRequest: (path: NormalisedURLPath, body: any, params: any, userContext: any) => Promise; + sendPostRequest: (path: NormalisedURLPath, body: any, userContext: UserContext) => Promise; + sendDeleteRequest: ( + path: NormalisedURLPath, + body: any, + params: any | undefined, + userContext: UserContext + ) => Promise; sendGetRequest: ( path: NormalisedURLPath, params: Record, - userContext: any + userContext: UserContext ) => Promise; sendGetRequestWithResponseHeaders: ( path: NormalisedURLPath, params: Record, - userContext: any + userContext: UserContext ) => Promise<{ body: any; headers: Headers; }>; - sendPutRequest: (path: NormalisedURLPath, body: any, userContext: any) => Promise; + sendPutRequest: (path: NormalisedURLPath, body: any, userContext: UserContext) => Promise; + invalidateCoreCallCache: (userContext: UserContext) => void; getAllCoreUrlsForPath(path: string): string[]; private sendRequestHelper; } diff --git a/lib/build/querier.js b/lib/build/querier.js index 78e4e9fc4..9f233c862 100644 --- a/lib/build/querier.js +++ b/lib/build/querier.js @@ -78,6 +78,7 @@ class Querier { // path should start with "/" this.sendPostRequest = async (path, body, userContext) => { var _a; + this.invalidateCoreCallCache(userContext); const { body: respBody } = await this.sendRequestHelper( path, "POST", @@ -122,6 +123,7 @@ class Querier { // path should start with "/" this.sendDeleteRequest = async (path, body, params, userContext) => { var _a; + this.invalidateCoreCallCache(userContext); const { body: respBody } = await this.sendRequestHelper( path, "DELETE", @@ -169,7 +171,22 @@ class Querier { }; // path should start with "/" this.sendGetRequest = async (path, params, userContext) => { - var _a; + var _a, _b, _c, _d; + const sortedKeys = Object.keys(params).sort(); + let uniqueKey = path.getAsStringDangerous(); + for (const key of sortedKeys) { + const value = params[key]; + uniqueKey += `;${key}=${value}`; + } + if ( + uniqueKey in + ((_b = (_a = userContext._default) === null || _a === void 0 ? void 0 : _a.coreCallCache) !== null && + _b !== void 0 + ? _b + : {}) + ) { + return userContext._default.coreCallCache[uniqueKey]; + } const { body: respBody } = await this.sendRequestHelper( path, "GET", @@ -208,8 +225,17 @@ class Querier { headers, }); }, - ((_a = this.__hosts) === null || _a === void 0 ? void 0 : _a.length) || 0 + ((_c = this.__hosts) === null || _c === void 0 ? void 0 : _c.length) || 0 ); + userContext._default = Object.assign(Object.assign({}, userContext._default), { + coreCallCache: Object.assign( + Object.assign( + {}, + (_d = userContext._default) === null || _d === void 0 ? void 0 : _d.coreCallCache + ), + { [uniqueKey]: respBody } + ), + }); return respBody; }; this.sendGetRequestWithResponseHeaders = async (path, params, userContext) => { @@ -258,6 +284,7 @@ class Querier { // path should start with "/" this.sendPutRequest = async (path, body, userContext) => { var _a; + this.invalidateCoreCallCache(userContext); const { body: respBody } = await this.sendRequestHelper( path, "PUT", @@ -296,6 +323,9 @@ class Querier { ); return respBody; }; + this.invalidateCoreCallCache = (userContext) => { + userContext._default = Object.assign(Object.assign({}, userContext._default), { coreCallCache: {} }); + }; // path should start with "/" this.sendRequestHelper = async (path, method, requestFunc, numberOfTries, retryInfoMap) => { var _a; @@ -323,6 +353,7 @@ class Querier { processState_1.ProcessState.getInstance().addState( processState_1.PROCESS_STATE.CALLING_SERVICE_IN_REQUEST_HELPER ); + logger_1.logDebugMessage(`core-call: ${method} ${url}`); let response = await requestFunc(url); if (process.env.TEST_MODE === "testing") { Querier.hostsAliveForTesting.add(currentDomain + currentBasePath); diff --git a/lib/build/recipe/accountlinking/index.d.ts b/lib/build/recipe/accountlinking/index.d.ts index 79ac077be..fbce90bce 100644 --- a/lib/build/recipe/accountlinking/index.d.ts +++ b/lib/build/recipe/accountlinking/index.d.ts @@ -16,7 +16,7 @@ export default class Wrapper { static createPrimaryUserIdOrLinkAccounts( tenantId: string, recipeUserId: RecipeUserId, - userContext?: any + userContext?: Record ): Promise; /** * This function returns the primary user that the input recipe ID can be @@ -30,11 +30,11 @@ export default class Wrapper { static getPrimaryUserThatCanBeLinkedToRecipeUserId( tenantId: string, recipeUserId: RecipeUserId, - userContext?: any + userContext?: Record ): Promise; static canCreatePrimaryUser( recipeUserId: RecipeUserId, - userContext?: any + userContext?: Record ): Promise< | { status: "OK"; @@ -50,7 +50,7 @@ export default class Wrapper { >; static createPrimaryUser( recipeUserId: RecipeUserId, - userContext?: any + userContext?: Record ): Promise< | { status: "OK"; @@ -70,7 +70,7 @@ export default class Wrapper { static canLinkAccounts( recipeUserId: RecipeUserId, primaryUserId: string, - userContext?: any + userContext?: Record ): Promise< | { status: "OK"; @@ -93,7 +93,7 @@ export default class Wrapper { static linkAccounts( recipeUserId: RecipeUserId, primaryUserId: string, - userContext?: any + userContext?: Record ): Promise< | { status: "OK"; @@ -116,7 +116,7 @@ export default class Wrapper { >; static unlinkAccount( recipeUserId: RecipeUserId, - userContext?: any + userContext?: Record ): Promise<{ status: "OK"; wasRecipeUserDeleted: boolean; @@ -126,14 +126,18 @@ export default class Wrapper { tenantId: string, newUser: AccountInfoWithRecipeId, isVerified: boolean, - userContext?: any + userContext?: Record + ): Promise; + static isSignInAllowed( + tenantId: string, + recipeUserId: RecipeUserId, + userContext?: Record ): Promise; - static isSignInAllowed(tenantId: string, recipeUserId: RecipeUserId, userContext?: any): Promise; static isEmailChangeAllowed( recipeUserId: RecipeUserId, newEmail: string, isVerified: boolean, - userContext?: any + userContext?: Record ): Promise; } export declare const init: typeof Recipe.init; diff --git a/lib/build/recipe/accountlinking/index.js b/lib/build/recipe/accountlinking/index.js index fb878fbfc..31df34df3 100644 --- a/lib/build/recipe/accountlinking/index.js +++ b/lib/build/recipe/accountlinking/index.js @@ -22,6 +22,7 @@ Object.defineProperty(exports, "__esModule", { value: true }); exports.isEmailChangeAllowed = exports.isSignInAllowed = exports.isSignUpAllowed = exports.getPrimaryUserThatCanBeLinkedToRecipeUserId = exports.createPrimaryUserIdOrLinkAccounts = exports.unlinkAccount = exports.linkAccounts = exports.canLinkAccounts = exports.createPrimaryUser = exports.canCreatePrimaryUser = exports.init = void 0; const recipe_1 = __importDefault(require("./recipe")); const __1 = require("../.."); +const utils_1 = require("../../utils"); class Wrapper { /** * This is a function which is a combination of createPrimaryUser and @@ -32,7 +33,7 @@ class Wrapper { * same as the input recipeUserId if it was made into a primary user, or if there was * no linking that happened. */ - static async createPrimaryUserIdOrLinkAccounts(tenantId, recipeUserId, userContext = {}) { + static async createPrimaryUserIdOrLinkAccounts(tenantId, recipeUserId, userContext) { const user = await __1.getUser(recipeUserId.getAsString(), userContext); if (user === undefined) { // Should never really come here unless a programming error happened in the app @@ -41,7 +42,7 @@ class Wrapper { return await recipe_1.default.getInstance().createPrimaryUserIdOrLinkAccounts({ tenantId, user, - userContext, + userContext: utils_1.getUserContext(userContext), }); } /** @@ -53,7 +54,7 @@ class Wrapper { * that the input recipe ID can be linked to, and therefore it can be made * into a primary user itself. */ - static async getPrimaryUserThatCanBeLinkedToRecipeUserId(tenantId, recipeUserId, userContext = {}) { + static async getPrimaryUserThatCanBeLinkedToRecipeUserId(tenantId, recipeUserId, userContext) { const user = await __1.getUser(recipeUserId.getAsString(), userContext); if (user === undefined) { // Should never really come here unless a programming error happened in the app @@ -62,39 +63,39 @@ class Wrapper { return await recipe_1.default.getInstance().getPrimaryUserThatCanBeLinkedToRecipeUserId({ tenantId, user, - userContext, + userContext: utils_1.getUserContext(userContext), }); } - static async canCreatePrimaryUser(recipeUserId, userContext = {}) { + static async canCreatePrimaryUser(recipeUserId, userContext) { return await recipe_1.default.getInstance().recipeInterfaceImpl.canCreatePrimaryUser({ recipeUserId, - userContext, + userContext: utils_1.getUserContext(userContext), }); } - static async createPrimaryUser(recipeUserId, userContext = {}) { + static async createPrimaryUser(recipeUserId, userContext) { return await recipe_1.default.getInstance().recipeInterfaceImpl.createPrimaryUser({ recipeUserId, - userContext, + userContext: utils_1.getUserContext(userContext), }); } - static async canLinkAccounts(recipeUserId, primaryUserId, userContext = {}) { + static async canLinkAccounts(recipeUserId, primaryUserId, userContext) { return await recipe_1.default.getInstance().recipeInterfaceImpl.canLinkAccounts({ recipeUserId, primaryUserId, - userContext, + userContext: utils_1.getUserContext(userContext), }); } - static async linkAccounts(recipeUserId, primaryUserId, userContext = {}) { + static async linkAccounts(recipeUserId, primaryUserId, userContext) { return await recipe_1.default.getInstance().recipeInterfaceImpl.linkAccounts({ recipeUserId, primaryUserId, - userContext, + userContext: utils_1.getUserContext(userContext), }); } - static async unlinkAccount(recipeUserId, userContext = {}) { + static async unlinkAccount(recipeUserId, userContext) { return await recipe_1.default.getInstance().recipeInterfaceImpl.unlinkAccount({ recipeUserId, - userContext, + userContext: utils_1.getUserContext(userContext), }); } static async isSignUpAllowed(tenantId, newUser, isVerified, userContext) { @@ -102,10 +103,10 @@ class Wrapper { newUser, isVerified, tenantId, - userContext, + userContext: utils_1.getUserContext(userContext), }); } - static async isSignInAllowed(tenantId, recipeUserId, userContext = {}) { + static async isSignInAllowed(tenantId, recipeUserId, userContext) { const user = await __1.getUser(recipeUserId.getAsString(), userContext); if (user === undefined) { // Should never really come here unless a programming error happened in the app @@ -114,7 +115,7 @@ class Wrapper { return await recipe_1.default.getInstance().isSignInAllowed({ user, tenantId, - userContext, + userContext: utils_1.getUserContext(userContext), }); } static async isEmailChangeAllowed(recipeUserId, newEmail, isVerified, userContext) { @@ -123,7 +124,7 @@ class Wrapper { user, newEmail, isVerified, - userContext, + userContext: utils_1.getUserContext(userContext), }); } } diff --git a/lib/build/recipe/accountlinking/recipe.d.ts b/lib/build/recipe/accountlinking/recipe.d.ts index 8c89d4643..708bc4a39 100644 --- a/lib/build/recipe/accountlinking/recipe.d.ts +++ b/lib/build/recipe/accountlinking/recipe.d.ts @@ -3,9 +3,9 @@ import error from "../../error"; import type { BaseRequest, BaseResponse } from "../../framework"; import normalisedURLPath from "../../normalisedURLPath"; import RecipeModule from "../../recipeModule"; -import type { APIHandled, HTTPMethod, NormalisedAppinfo, RecipeListFunction, User } from "../../types"; +import type { APIHandled, HTTPMethod, NormalisedAppinfo, RecipeListFunction, User, UserContext } from "../../types"; import type { TypeNormalisedInput, RecipeInterface, TypeInput, AccountInfoWithRecipeId } from "./types"; -import RecipeUserId from "../../recipeUserId"; +import { verifyEmailForRecipeUserIfLinkedAccountsAreVerified } from "./utils"; import { LoginMethod } from "../../user"; export default class Recipe extends RecipeModule { private static instance; @@ -41,7 +41,7 @@ export default class Recipe extends RecipeModule { }: { tenantId: string; user: User; - userContext: any; + userContext: UserContext; }) => Promise; getPrimaryUserThatCanBeLinkedToRecipeUserId: ({ tenantId, @@ -50,7 +50,7 @@ export default class Recipe extends RecipeModule { }: { tenantId: string; user: User; - userContext: any; + userContext: UserContext; }) => Promise; isSignInAllowed: ({ user, @@ -59,7 +59,7 @@ export default class Recipe extends RecipeModule { }: { user: User; tenantId: string; - userContext: any; + userContext: UserContext; }) => Promise; isSignUpAllowed: ({ newUser, @@ -70,7 +70,7 @@ export default class Recipe extends RecipeModule { newUser: AccountInfoWithRecipeId; isVerified: boolean; tenantId: string; - userContext: any; + userContext: UserContext; }) => Promise; isSignInUpAllowedHelper: ({ accountInfo, @@ -83,17 +83,13 @@ export default class Recipe extends RecipeModule { isVerified: boolean; tenantId: string; isSignIn: boolean; - userContext: any; + userContext: UserContext; }) => Promise; isEmailChangeAllowed: (input: { user?: User; newEmail: string; isVerified: boolean; - userContext: any; + userContext: UserContext; }) => Promise; - verifyEmailForRecipeUserIfLinkedAccountsAreVerified: (input: { - user: User; - recipeUserId: RecipeUserId; - userContext: any; - }) => Promise; + verifyEmailForRecipeUserIfLinkedAccountsAreVerified: typeof verifyEmailForRecipeUserIfLinkedAccountsAreVerified; } diff --git a/lib/build/recipe/accountlinking/recipe.js b/lib/build/recipe/accountlinking/recipe.js index b13ab3527..d8bc8ea9d 100644 --- a/lib/build/recipe/accountlinking/recipe.js +++ b/lib/build/recipe/accountlinking/recipe.js @@ -28,7 +28,6 @@ const error_1 = __importDefault(require("../../error")); const supertokens_1 = __importDefault(require("../../supertokens")); const processState_1 = require("../../processState"); const logger_1 = require("../../logger"); -const recipe_1 = __importDefault(require("../emailverification/recipe")); class Recipe extends recipeModule_1.default { constructor(recipeId, appInfo, config, _recipes, _ingredients) { super(recipeId, appInfo); @@ -552,68 +551,8 @@ class Recipe extends recipeModule_1.default { ); return true; }; - this.verifyEmailForRecipeUserIfLinkedAccountsAreVerified = async (input) => { - try { - recipe_1.default.getInstanceOrThrowError(); - } catch (ignored) { - // if email verification recipe is not initialized, we do a no-op - return; - } - // This is just a helper function cause it's called in many places - // like during sign up, sign in and post linking accounts. - // This is not exposed to the developer as it's called in the relevant - // recipe functions. - // We do not do this in the core cause email verification is a different - // recipe. - // Finally, we only mark the email of this recipe user as verified and not - // the other recipe users in the primary user (if this user's email is verified), - // cause when those other users sign in, this function will be called for them anyway - if (input.user.isPrimaryUser) { - let recipeUserEmail = undefined; - let isAlreadyVerified = false; - input.user.loginMethods.forEach((lm) => { - if (lm.recipeUserId.getAsString() === input.recipeUserId.getAsString()) { - recipeUserEmail = lm.email; - isAlreadyVerified = lm.verified; - } - }); - if (recipeUserEmail !== undefined) { - if (isAlreadyVerified) { - return; - } - let shouldVerifyEmail = false; - input.user.loginMethods.forEach((lm) => { - if (lm.hasSameEmailAs(recipeUserEmail) && lm.verified) { - shouldVerifyEmail = true; - } - }); - if (shouldVerifyEmail) { - let resp = await recipe_1.default - .getInstanceOrThrowError() - .recipeInterfaceImpl.createEmailVerificationToken({ - // While the token we create here is tenant specific, the verification status is not - // So we can use any tenantId the user is associated with here as long as we use the - // same in the verifyEmailUsingToken call - tenantId: input.user.tenantIds[0], - recipeUserId: input.recipeUserId, - email: recipeUserEmail, - userContext: input.userContext, - }); - if (resp.status === "OK") { - // we purposely pass in false below cause we don't want account - // linking to happen - await recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.verifyEmailUsingToken({ - // See comment about tenantId in the createEmailVerificationToken params - tenantId: input.user.tenantIds[0], - token: resp.token, - attemptAccountLinking: false, - userContext: input.userContext, - }); - } - } - } - } - }; + this.verifyEmailForRecipeUserIfLinkedAccountsAreVerified = + utils_1.verifyEmailForRecipeUserIfLinkedAccountsAreVerified; this.config = utils_1.validateAndNormaliseUserInput(appInfo, config); { let builder = new supertokens_js_override_1.default( diff --git a/lib/build/recipe/accountlinking/recipeImplementation.js b/lib/build/recipe/accountlinking/recipeImplementation.js index 55774660f..c1dc0d1db 100644 --- a/lib/build/recipe/accountlinking/recipeImplementation.js +++ b/lib/build/recipe/accountlinking/recipeImplementation.js @@ -152,7 +152,8 @@ function getRecipeImplementation(querier, config, recipeInstance) { userContext ); if (result.status === "OK") { - return new user_1.User(result.user); + const userResult = new user_1.User(result.user); + return userResult; } return undefined; }, diff --git a/lib/build/recipe/accountlinking/types.d.ts b/lib/build/recipe/accountlinking/types.d.ts index 804682e69..935b9a60e 100644 --- a/lib/build/recipe/accountlinking/types.d.ts +++ b/lib/build/recipe/accountlinking/types.d.ts @@ -1,16 +1,16 @@ // @ts-nocheck import OverrideableBuilder from "supertokens-js-override"; -import type { User } from "../../types"; +import type { User, UserContext } from "../../types"; import RecipeUserId from "../../recipeUserId"; export declare type TypeInput = { - onAccountLinked?: (user: User, newAccountInfo: RecipeLevelUser, userContext: any) => Promise; + onAccountLinked?: (user: User, newAccountInfo: RecipeLevelUser, userContext: UserContext) => Promise; shouldDoAutomaticAccountLinking?: ( newAccountInfo: AccountInfoWithRecipeId & { recipeUserId?: RecipeUserId; }, user: User | undefined, tenantId: string, - userContext: any + userContext: UserContext ) => Promise< | { shouldAutomaticallyLink: false; @@ -28,14 +28,14 @@ export declare type TypeInput = { }; }; export declare type TypeNormalisedInput = { - onAccountLinked: (user: User, newAccountInfo: RecipeLevelUser, userContext: any) => Promise; + onAccountLinked: (user: User, newAccountInfo: RecipeLevelUser, userContext: UserContext) => Promise; shouldDoAutomaticAccountLinking: ( newAccountInfo: AccountInfoWithRecipeId & { recipeUserId?: RecipeUserId; }, user: User | undefined, tenantId: string, - userContext: any + userContext: UserContext ) => Promise< | { shouldAutomaticallyLink: false; @@ -62,14 +62,14 @@ export declare type RecipeInterface = { query?: { [key: string]: string; }; - userContext: any; + userContext: UserContext; }) => Promise<{ users: User[]; nextPaginationToken?: string; }>; canCreatePrimaryUser: (input: { recipeUserId: RecipeUserId; - userContext: any; + userContext: UserContext; }) => Promise< | { status: "OK"; @@ -85,7 +85,7 @@ export declare type RecipeInterface = { >; createPrimaryUser: (input: { recipeUserId: RecipeUserId; - userContext: any; + userContext: UserContext; }) => Promise< | { status: "OK"; @@ -105,7 +105,7 @@ export declare type RecipeInterface = { canLinkAccounts: (input: { recipeUserId: RecipeUserId; primaryUserId: string; - userContext: any; + userContext: UserContext; }) => Promise< | { status: "OK"; @@ -128,7 +128,7 @@ export declare type RecipeInterface = { linkAccounts: (input: { recipeUserId: RecipeUserId; primaryUserId: string; - userContext: any; + userContext: UserContext; }) => Promise< | { status: "OK"; @@ -151,23 +151,23 @@ export declare type RecipeInterface = { >; unlinkAccount: (input: { recipeUserId: RecipeUserId; - userContext: any; + userContext: UserContext; }) => Promise<{ status: "OK"; wasRecipeUserDeleted: boolean; wasLinked: boolean; }>; - getUser: (input: { userId: string; userContext: any }) => Promise; + getUser: (input: { userId: string; userContext: UserContext }) => Promise; listUsersByAccountInfo: (input: { tenantId: string; accountInfo: AccountInfo; doUnionOfAccountInfo: boolean; - userContext: any; + userContext: UserContext; }) => Promise; deleteUser: (input: { userId: string; removeAllLinkedAccounts: boolean; - userContext: any; + userContext: UserContext; }) => Promise<{ status: "OK"; }>; diff --git a/lib/build/recipe/accountlinking/utils.d.ts b/lib/build/recipe/accountlinking/utils.d.ts index 8e838c081..9c4f4f97b 100644 --- a/lib/build/recipe/accountlinking/utils.d.ts +++ b/lib/build/recipe/accountlinking/utils.d.ts @@ -1,4 +1,11 @@ // @ts-nocheck -import type { NormalisedAppinfo } from "../../types"; +import RecipeUserId from "../../recipeUserId"; +import type { NormalisedAppinfo, UserContext } from "../../types"; +import { User } from "../../user"; import type { TypeInput, TypeNormalisedInput } from "./types"; export declare function validateAndNormaliseUserInput(_: NormalisedAppinfo, config?: TypeInput): TypeNormalisedInput; +export declare function verifyEmailForRecipeUserIfLinkedAccountsAreVerified(input: { + user: User; + recipeUserId: RecipeUserId; + userContext: UserContext; +}): Promise; diff --git a/lib/build/recipe/accountlinking/utils.js b/lib/build/recipe/accountlinking/utils.js index 24c346b43..5ae1e0aae 100644 --- a/lib/build/recipe/accountlinking/utils.js +++ b/lib/build/recipe/accountlinking/utils.js @@ -13,8 +13,14 @@ * License for the specific language governing permissions and limitations * under the License. */ +var __importDefault = + (this && this.__importDefault) || + function (mod) { + return mod && mod.__esModule ? mod : { default: mod }; + }; Object.defineProperty(exports, "__esModule", { value: true }); -exports.validateAndNormaliseUserInput = void 0; +exports.verifyEmailForRecipeUserIfLinkedAccountsAreVerified = exports.validateAndNormaliseUserInput = void 0; +const recipe_1 = __importDefault(require("../emailverification/recipe")); async function defaultOnAccountLinked() {} async function defaultShouldDoAutomaticAccountLinking() { return { @@ -38,3 +44,66 @@ function validateAndNormaliseUserInput(_, config) { }; } exports.validateAndNormaliseUserInput = validateAndNormaliseUserInput; +async function verifyEmailForRecipeUserIfLinkedAccountsAreVerified(input) { + try { + recipe_1.default.getInstanceOrThrowError(); + } catch (ignored) { + // if email verification recipe is not initialized, we do a no-op + return; + } + // This is just a helper function cause it's called in many places + // like during sign up, sign in and post linking accounts. + // This is not exposed to the developer as it's called in the relevant + // recipe functions. + // We do not do this in the core cause email verification is a different + // recipe. + // Finally, we only mark the email of this recipe user as verified and not + // the other recipe users in the primary user (if this user's email is verified), + // cause when those other users sign in, this function will be called for them anyway + if (input.user.isPrimaryUser) { + let recipeUserEmail = undefined; + let isAlreadyVerified = false; + input.user.loginMethods.forEach((lm) => { + if (lm.recipeUserId.getAsString() === input.recipeUserId.getAsString()) { + recipeUserEmail = lm.email; + isAlreadyVerified = lm.verified; + } + }); + if (recipeUserEmail !== undefined) { + if (isAlreadyVerified) { + return; + } + let shouldVerifyEmail = false; + input.user.loginMethods.forEach((lm) => { + if (lm.hasSameEmailAs(recipeUserEmail) && lm.verified) { + shouldVerifyEmail = true; + } + }); + if (shouldVerifyEmail) { + let resp = await recipe_1.default + .getInstanceOrThrowError() + .recipeInterfaceImpl.createEmailVerificationToken({ + // While the token we create here is tenant specific, the verification status is not + // So we can use any tenantId the user is associated with here as long as we use the + // same in the verifyEmailUsingToken call + tenantId: input.user.tenantIds[0], + recipeUserId: input.recipeUserId, + email: recipeUserEmail, + userContext: input.userContext, + }); + if (resp.status === "OK") { + // we purposely pass in false below cause we don't want account + // linking to happen + await recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.verifyEmailUsingToken({ + // See comment about tenantId in the createEmailVerificationToken params + tenantId: input.user.tenantIds[0], + token: resp.token, + attemptAccountLinking: false, + userContext: input.userContext, + }); + } + } + } + } +} +exports.verifyEmailForRecipeUserIfLinkedAccountsAreVerified = verifyEmailForRecipeUserIfLinkedAccountsAreVerified; diff --git a/lib/build/recipe/dashboard/api/analytics.d.ts b/lib/build/recipe/dashboard/api/analytics.d.ts index 0c7cca8f9..1b0c81d49 100644 --- a/lib/build/recipe/dashboard/api/analytics.d.ts +++ b/lib/build/recipe/dashboard/api/analytics.d.ts @@ -1,5 +1,6 @@ // @ts-nocheck import { APIInterface, APIOptions } from "../types"; +import { UserContext } from "../../../types"; export declare type Response = { status: "OK"; }; @@ -7,5 +8,5 @@ export default function analyticsPost( _: APIInterface, ___: string, options: APIOptions, - userContext: any + userContext: UserContext ): Promise; diff --git a/lib/build/recipe/dashboard/api/analytics.js b/lib/build/recipe/dashboard/api/analytics.js index 2a3652198..18762e156 100644 --- a/lib/build/recipe/dashboard/api/analytics.js +++ b/lib/build/recipe/dashboard/api/analytics.js @@ -53,7 +53,9 @@ async function analyticsPost(_, ___, options, userContext) { if (response.exists) { telemetryId = response.telemetryId; } - numberOfUsers = await supertokens_1.default.getInstanceOrThrowError().getUserCount(); + numberOfUsers = await supertokens_1.default + .getInstanceOrThrowError() + .getUserCount(undefined, undefined, userContext); } catch (_) { // If either telemetry id API or user count fetch fails, no event should be sent return { @@ -64,7 +66,7 @@ async function analyticsPost(_, ___, options, userContext) { const data = { websiteDomain: websiteDomain({ request: undefined, - userContext: {}, + userContext, }).getAsStringDangerous(), apiDomain: apiDomain.getAsStringDangerous(), appName, diff --git a/lib/build/recipe/dashboard/api/apiKeyProtector.d.ts b/lib/build/recipe/dashboard/api/apiKeyProtector.d.ts index 357ebeb82..eb8ad8d0e 100644 --- a/lib/build/recipe/dashboard/api/apiKeyProtector.d.ts +++ b/lib/build/recipe/dashboard/api/apiKeyProtector.d.ts @@ -1,9 +1,10 @@ // @ts-nocheck +import { UserContext } from "../../../types"; import { APIFunction, APIInterface, APIOptions } from "../types"; export default function apiKeyProtector( apiImplementation: APIInterface, tenantId: string, options: APIOptions, apiFunction: APIFunction, - userContext: any + userContext: UserContext ): Promise; diff --git a/lib/build/recipe/dashboard/api/apiKeyProtector.js b/lib/build/recipe/dashboard/api/apiKeyProtector.js index 3437e153b..50a9d7031 100644 --- a/lib/build/recipe/dashboard/api/apiKeyProtector.js +++ b/lib/build/recipe/dashboard/api/apiKeyProtector.js @@ -5,20 +5,6 @@ var __importDefault = return mod && mod.__esModule ? mod : { default: mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); -/* Copyright (c) 2022, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ const error_1 = __importDefault(require("../error")); const utils_1 = require("../utils"); async function apiKeyProtector(apiImplementation, tenantId, options, apiFunction, userContext) { diff --git a/lib/build/recipe/dashboard/api/dashboard.d.ts b/lib/build/recipe/dashboard/api/dashboard.d.ts index 90cd67f50..252a4acf3 100644 --- a/lib/build/recipe/dashboard/api/dashboard.d.ts +++ b/lib/build/recipe/dashboard/api/dashboard.d.ts @@ -1,7 +1,8 @@ // @ts-nocheck +import { UserContext } from "../../../types"; import { APIInterface, APIOptions } from "../types"; export default function dashboard( apiImplementation: APIInterface, options: APIOptions, - userContext: any + userContext: UserContext ): Promise; diff --git a/lib/build/recipe/dashboard/api/listTenants.d.ts b/lib/build/recipe/dashboard/api/listTenants.d.ts index 3b7422fa8..8273e46a0 100644 --- a/lib/build/recipe/dashboard/api/listTenants.d.ts +++ b/lib/build/recipe/dashboard/api/listTenants.d.ts @@ -1,6 +1,7 @@ // @ts-nocheck import { APIInterface, APIOptions } from "../types"; import { ProviderConfig } from "../../thirdparty/types"; +import { UserContext } from "../../../types"; declare type TenantListTenantType = { tenantId: string; emailPassword: { @@ -18,5 +19,10 @@ export declare type Response = { status: "OK"; tenants: TenantListTenantType[]; }; -export default function listTenants(_: APIInterface, __: string, ___: APIOptions, userContext: any): Promise; +export default function listTenants( + _: APIInterface, + __: string, + ___: APIOptions, + userContext: UserContext +): Promise; export {}; diff --git a/lib/build/recipe/dashboard/api/search/tagsGet.d.ts b/lib/build/recipe/dashboard/api/search/tagsGet.d.ts index 1e12e1107..68ed9b3fe 100644 --- a/lib/build/recipe/dashboard/api/search/tagsGet.d.ts +++ b/lib/build/recipe/dashboard/api/search/tagsGet.d.ts @@ -1,5 +1,6 @@ // @ts-nocheck import { APIInterface, APIOptions } from "../../types"; +import { UserContext } from "../../../../types"; declare type TagsResponse = { status: "OK"; tags: string[]; @@ -8,6 +9,6 @@ export declare const getSearchTags: ( _: APIInterface, ___: string, options: APIOptions, - userContext: any + userContext: UserContext ) => Promise; export {}; diff --git a/lib/build/recipe/dashboard/api/signIn.d.ts b/lib/build/recipe/dashboard/api/signIn.d.ts index 7bf92bacc..42ca0fba3 100644 --- a/lib/build/recipe/dashboard/api/signIn.d.ts +++ b/lib/build/recipe/dashboard/api/signIn.d.ts @@ -1,3 +1,4 @@ // @ts-nocheck import { APIInterface, APIOptions } from "../types"; -export default function signIn(_: APIInterface, options: APIOptions, userContext: any): Promise; +import { UserContext } from "../../../types"; +export default function signIn(_: APIInterface, options: APIOptions, userContext: UserContext): Promise; diff --git a/lib/build/recipe/dashboard/api/signOut.d.ts b/lib/build/recipe/dashboard/api/signOut.d.ts index 9e17486be..b10c8509a 100644 --- a/lib/build/recipe/dashboard/api/signOut.d.ts +++ b/lib/build/recipe/dashboard/api/signOut.d.ts @@ -1,3 +1,9 @@ // @ts-nocheck import { APIInterface, APIOptions } from "../types"; -export default function signOut(_: APIInterface, ___: string, options: APIOptions, userContext: any): Promise; +import { UserContext } from "../../../types"; +export default function signOut( + _: APIInterface, + ___: string, + options: APIOptions, + userContext: UserContext +): Promise; diff --git a/lib/build/recipe/dashboard/api/userdetails/userEmailVerifyPut.d.ts b/lib/build/recipe/dashboard/api/userdetails/userEmailVerifyPut.d.ts index 5f2b605d5..9b92ba163 100644 --- a/lib/build/recipe/dashboard/api/userdetails/userEmailVerifyPut.d.ts +++ b/lib/build/recipe/dashboard/api/userdetails/userEmailVerifyPut.d.ts @@ -1,5 +1,6 @@ // @ts-nocheck import { APIInterface, APIOptions } from "../../types"; +import { UserContext } from "../../../../types"; declare type Response = { status: "OK"; }; @@ -7,6 +8,6 @@ export declare const userEmailVerifyPut: ( _: APIInterface, tenantId: string, options: APIOptions, - userContext: any + userContext: UserContext ) => Promise; export {}; diff --git a/lib/build/recipe/dashboard/api/userdetails/userEmailVerifyPut.js b/lib/build/recipe/dashboard/api/userdetails/userEmailVerifyPut.js index c7fb3b928..604be3c27 100644 --- a/lib/build/recipe/dashboard/api/userdetails/userEmailVerifyPut.js +++ b/lib/build/recipe/dashboard/api/userdetails/userEmailVerifyPut.js @@ -40,6 +40,7 @@ const userEmailVerifyPut = async (_, tenantId, options, userContext) => { const verifyResponse = await emailverification_1.default.verifyEmailUsingToken( tenantId, tokenResponse.token, + undefined, userContext ); if (verifyResponse.status === "EMAIL_VERIFICATION_INVALID_TOKEN_ERROR") { diff --git a/lib/build/recipe/dashboard/api/userdetails/userEmailVerifyTokenPost.d.ts b/lib/build/recipe/dashboard/api/userdetails/userEmailVerifyTokenPost.d.ts index 68039149d..16b68a15c 100644 --- a/lib/build/recipe/dashboard/api/userdetails/userEmailVerifyTokenPost.d.ts +++ b/lib/build/recipe/dashboard/api/userdetails/userEmailVerifyTokenPost.d.ts @@ -1,5 +1,6 @@ // @ts-nocheck import { APIInterface, APIOptions } from "../../types"; +import { UserContext } from "../../../../types"; declare type Response = { status: "OK" | "EMAIL_ALREADY_VERIFIED_ERROR"; }; @@ -7,6 +8,6 @@ export declare const userEmailVerifyTokenPost: ( _: APIInterface, tenantId: string, options: APIOptions, - userContext: any + userContext: UserContext ) => Promise; export {}; diff --git a/lib/build/recipe/dashboard/api/userdetails/userMetadataPut.d.ts b/lib/build/recipe/dashboard/api/userdetails/userMetadataPut.d.ts index 0fa8dac6b..f6099dc09 100644 --- a/lib/build/recipe/dashboard/api/userdetails/userMetadataPut.d.ts +++ b/lib/build/recipe/dashboard/api/userdetails/userMetadataPut.d.ts @@ -1,5 +1,6 @@ // @ts-nocheck import { APIInterface, APIOptions } from "../../types"; +import { UserContext } from "../../../../types"; declare type Response = { status: "OK"; }; @@ -7,6 +8,6 @@ export declare const userMetadataPut: ( _: APIInterface, ___: string, options: APIOptions, - userContext: any + userContext: UserContext ) => Promise; export {}; diff --git a/lib/build/recipe/dashboard/api/userdetails/userPasswordPut.d.ts b/lib/build/recipe/dashboard/api/userdetails/userPasswordPut.d.ts index f2cbf6abd..eb0b5a928 100644 --- a/lib/build/recipe/dashboard/api/userdetails/userPasswordPut.d.ts +++ b/lib/build/recipe/dashboard/api/userdetails/userPasswordPut.d.ts @@ -1,5 +1,6 @@ // @ts-nocheck import { APIInterface, APIOptions } from "../../types"; +import { UserContext } from "../../../../types"; declare type Response = | { status: "OK"; @@ -12,6 +13,6 @@ export declare const userPasswordPut: ( _: APIInterface, tenantId: string, options: APIOptions, - userContext: any + userContext: UserContext ) => Promise; export {}; diff --git a/lib/build/recipe/dashboard/api/userdetails/userPut.d.ts b/lib/build/recipe/dashboard/api/userdetails/userPut.d.ts index 737bae4d1..b4156bb5c 100644 --- a/lib/build/recipe/dashboard/api/userdetails/userPut.d.ts +++ b/lib/build/recipe/dashboard/api/userdetails/userPut.d.ts @@ -1,5 +1,6 @@ // @ts-nocheck import { APIInterface, APIOptions } from "../../types"; +import { UserContext } from "../../../../types"; declare type Response = | { status: "OK"; @@ -30,6 +31,6 @@ export declare const userPut: ( _: APIInterface, tenantId: string, options: APIOptions, - userContext: any + userContext: UserContext ) => Promise; export {}; diff --git a/lib/build/recipe/dashboard/api/userdetails/userPut.js b/lib/build/recipe/dashboard/api/userdetails/userPut.js index 642c427fb..3465dfb53 100644 --- a/lib/build/recipe/dashboard/api/userdetails/userPut.js +++ b/lib/build/recipe/dashboard/api/userdetails/userPut.js @@ -322,7 +322,11 @@ const userPut = async (_, tenantId, options, userContext) => { type: error_1.default.BAD_INPUT_ERROR, }); } - let userResponse = await utils_1.getUserForRecipeId(new recipeUserId_1.default(recipeUserId), recipeId); + let userResponse = await utils_1.getUserForRecipeId( + new recipeUserId_1.default(recipeUserId), + recipeId, + userContext + ); if (userResponse.user === undefined || userResponse.recipe === undefined) { throw new Error("Should never come here"); } diff --git a/lib/build/recipe/dashboard/api/userdetails/userSessionsPost.d.ts b/lib/build/recipe/dashboard/api/userdetails/userSessionsPost.d.ts index cc1b697dc..1e9a3d118 100644 --- a/lib/build/recipe/dashboard/api/userdetails/userSessionsPost.d.ts +++ b/lib/build/recipe/dashboard/api/userdetails/userSessionsPost.d.ts @@ -1,5 +1,6 @@ // @ts-nocheck import { APIInterface, APIOptions } from "../../types"; +import { UserContext } from "../../../../types"; declare type Response = { status: "OK"; }; @@ -7,6 +8,6 @@ export declare const userSessionsPost: ( _: APIInterface, ___: string, options: APIOptions, - userContext: any + userContext: UserContext ) => Promise; export {}; diff --git a/lib/build/recipe/dashboard/api/userdetails/userUnlinkGet.d.ts b/lib/build/recipe/dashboard/api/userdetails/userUnlinkGet.d.ts index 8d94598ea..15b661f25 100644 --- a/lib/build/recipe/dashboard/api/userdetails/userUnlinkGet.d.ts +++ b/lib/build/recipe/dashboard/api/userdetails/userUnlinkGet.d.ts @@ -1,5 +1,6 @@ // @ts-nocheck import { APIInterface, APIOptions } from "../../types"; +import { UserContext } from "../../../../types"; declare type Response = { status: "OK"; }; @@ -7,6 +8,6 @@ export declare const userUnlink: ( _: APIInterface, ___: string, options: APIOptions, - userContext: any + userContext: UserContext ) => Promise; export {}; diff --git a/lib/build/recipe/dashboard/api/usersCountGet.d.ts b/lib/build/recipe/dashboard/api/usersCountGet.d.ts index 78b011594..2f90538df 100644 --- a/lib/build/recipe/dashboard/api/usersCountGet.d.ts +++ b/lib/build/recipe/dashboard/api/usersCountGet.d.ts @@ -1,7 +1,13 @@ // @ts-nocheck import { APIInterface, APIOptions } from "../types"; +import { UserContext } from "../../../types"; export declare type Response = { status: "OK"; count: number; }; -export default function usersCountGet(_: APIInterface, tenantId: string, __: APIOptions, ___: any): Promise; +export default function usersCountGet( + _: APIInterface, + tenantId: string, + __: APIOptions, + userContext: UserContext +): Promise; diff --git a/lib/build/recipe/dashboard/api/usersCountGet.js b/lib/build/recipe/dashboard/api/usersCountGet.js index 20b359227..55757ab2d 100644 --- a/lib/build/recipe/dashboard/api/usersCountGet.js +++ b/lib/build/recipe/dashboard/api/usersCountGet.js @@ -20,8 +20,8 @@ var __importDefault = }; Object.defineProperty(exports, "__esModule", { value: true }); const supertokens_1 = __importDefault(require("../../../supertokens")); -async function usersCountGet(_, tenantId, __, ___) { - const count = await supertokens_1.default.getInstanceOrThrowError().getUserCount(undefined, tenantId); +async function usersCountGet(_, tenantId, __, userContext) { + const count = await supertokens_1.default.getInstanceOrThrowError().getUserCount(undefined, tenantId, userContext); return { status: "OK", count, diff --git a/lib/build/recipe/dashboard/api/usersGet.d.ts b/lib/build/recipe/dashboard/api/usersGet.d.ts index 4a07467ad..42b2f0a50 100644 --- a/lib/build/recipe/dashboard/api/usersGet.d.ts +++ b/lib/build/recipe/dashboard/api/usersGet.d.ts @@ -1,5 +1,6 @@ // @ts-nocheck import { APIInterface, APIOptions, UserWithFirstAndLastName } from "../types"; +import { UserContext } from "../../../types"; export declare type Response = { status: "OK"; nextPaginationToken?: string; @@ -9,7 +10,7 @@ export default function usersGet( _: APIInterface, tenantId: string, options: APIOptions, - userContext: any + userContext: UserContext ): Promise; export declare function getSearchParamsFromURL( path: string diff --git a/lib/build/recipe/dashboard/api/validateKey.d.ts b/lib/build/recipe/dashboard/api/validateKey.d.ts index 0e7cbf759..fe0458621 100644 --- a/lib/build/recipe/dashboard/api/validateKey.d.ts +++ b/lib/build/recipe/dashboard/api/validateKey.d.ts @@ -1,3 +1,4 @@ // @ts-nocheck +import { UserContext } from "../../../types"; import { APIInterface, APIOptions } from "../types"; -export default function validateKey(_: APIInterface, options: APIOptions, userContext: any): Promise; +export default function validateKey(_: APIInterface, options: APIOptions, userContext: UserContext): Promise; diff --git a/lib/build/recipe/dashboard/recipe.d.ts b/lib/build/recipe/dashboard/recipe.d.ts index 1747e1482..8fbe6583e 100644 --- a/lib/build/recipe/dashboard/recipe.d.ts +++ b/lib/build/recipe/dashboard/recipe.d.ts @@ -1,6 +1,6 @@ // @ts-nocheck import RecipeModule from "../../recipeModule"; -import { APIHandled, HTTPMethod, NormalisedAppinfo, RecipeListFunction } from "../../types"; +import { APIHandled, HTTPMethod, NormalisedAppinfo, RecipeListFunction, UserContext } from "../../types"; import { APIInterface, RecipeInterface, TypeInput, TypeNormalisedInput } from "./types"; import NormalisedURLPath from "../../normalisedURLPath"; import type { BaseRequest, BaseResponse } from "../../framework"; @@ -24,7 +24,7 @@ export default class Recipe extends RecipeModule { res: BaseResponse, __: NormalisedURLPath, ___: HTTPMethod, - userContext: any + userContext: UserContext ) => Promise; handleError: (err: error, _: BaseRequest, __: BaseResponse) => Promise; getAllCORSHeaders: () => string[]; diff --git a/lib/build/recipe/dashboard/types.d.ts b/lib/build/recipe/dashboard/types.d.ts index def30a57a..866b48c65 100644 --- a/lib/build/recipe/dashboard/types.d.ts +++ b/lib/build/recipe/dashboard/types.d.ts @@ -1,7 +1,7 @@ // @ts-nocheck import OverrideableBuilder from "supertokens-js-override"; import type { BaseRequest, BaseResponse } from "../../framework"; -import { NormalisedAppinfo, User } from "../../types"; +import { NormalisedAppinfo, User, UserContext } from "../../types"; export declare type TypeInput = { apiKey?: string; admins?: string[]; @@ -26,8 +26,12 @@ export declare type TypeNormalisedInput = { }; }; export declare type RecipeInterface = { - getDashboardBundleLocation(input: { userContext: any }): Promise; - shouldAllowAccess(input: { req: BaseRequest; config: TypeNormalisedInput; userContext: any }): Promise; + getDashboardBundleLocation(input: { userContext: UserContext }): Promise; + shouldAllowAccess(input: { + req: BaseRequest; + config: TypeNormalisedInput; + userContext: UserContext; + }): Promise; }; export declare type APIOptions = { recipeImplementation: RecipeInterface; @@ -39,13 +43,13 @@ export declare type APIOptions = { appInfo: NormalisedAppinfo; }; export declare type APIInterface = { - dashboardGET: undefined | ((input: { options: APIOptions; userContext: any }) => Promise); + dashboardGET: undefined | ((input: { options: APIOptions; userContext: UserContext }) => Promise); }; export declare type APIFunction = ( apiImplementation: APIInterface, tenantId: string, options: APIOptions, - userContext: any + userContext: UserContext ) => Promise; export declare type RecipeIdForUser = "emailpassword" | "thirdparty" | "passwordless"; export declare type AuthMode = "api-key" | "email-password"; diff --git a/lib/build/recipe/dashboard/utils.d.ts b/lib/build/recipe/dashboard/utils.d.ts index 15877f8ca..48f60ec2d 100644 --- a/lib/build/recipe/dashboard/utils.d.ts +++ b/lib/build/recipe/dashboard/utils.d.ts @@ -2,12 +2,14 @@ import type { BaseRequest, BaseResponse } from "../../framework"; import { RecipeIdForUser, TypeInput, TypeNormalisedInput, UserWithFirstAndLastName } from "./types"; import RecipeUserId from "../../recipeUserId"; +import { UserContext } from "../../types"; export declare function validateAndNormaliseUserInput(config?: TypeInput): TypeNormalisedInput; export declare function sendUnauthorisedAccess(res: BaseResponse): void; export declare function isValidRecipeId(recipeId: string): recipeId is RecipeIdForUser; export declare function getUserForRecipeId( recipeUserId: RecipeUserId, - recipeId: string + recipeId: string, + userContext: UserContext ): Promise<{ user: UserWithFirstAndLastName | undefined; recipe: @@ -22,6 +24,6 @@ export declare function isRecipeInitialised(recipeId: RecipeIdForUser): boolean; export declare function validateApiKey(input: { req: BaseRequest; config: TypeNormalisedInput; - userContext: any; + userContext: UserContext; }): Promise; export declare function getApiPathWithDashboardBase(path: string): string; diff --git a/lib/build/recipe/dashboard/utils.js b/lib/build/recipe/dashboard/utils.js index ca68fa91b..6ed6ffd68 100644 --- a/lib/build/recipe/dashboard/utils.js +++ b/lib/build/recipe/dashboard/utils.js @@ -62,8 +62,8 @@ function isValidRecipeId(recipeId) { return recipeId === "emailpassword" || recipeId === "thirdparty" || recipeId === "passwordless"; } exports.isValidRecipeId = isValidRecipeId; -async function getUserForRecipeId(recipeUserId, recipeId) { - let userResponse = await _getUserForRecipeId(recipeUserId, recipeId); +async function getUserForRecipeId(recipeUserId, recipeId, userContext) { + let userResponse = await _getUserForRecipeId(recipeUserId, recipeId, userContext); let user = undefined; if (userResponse.user !== undefined) { user = Object.assign(Object.assign({}, userResponse.user), { firstName: "", lastName: "" }); @@ -74,11 +74,11 @@ async function getUserForRecipeId(recipeUserId, recipeId) { }; } exports.getUserForRecipeId = getUserForRecipeId; -async function _getUserForRecipeId(recipeUserId, recipeId) { +async function _getUserForRecipeId(recipeUserId, recipeId, userContext) { let recipe; const user = await recipe_1.default.getInstance().recipeInterfaceImpl.getUser({ userId: recipeUserId.getAsString(), - userContext: {}, + userContext, }); if (user === undefined) { return { diff --git a/lib/build/recipe/emailpassword/api/emailExists.d.ts b/lib/build/recipe/emailpassword/api/emailExists.d.ts index 42894445f..2f55b6d3b 100644 --- a/lib/build/recipe/emailpassword/api/emailExists.d.ts +++ b/lib/build/recipe/emailpassword/api/emailExists.d.ts @@ -1,8 +1,9 @@ // @ts-nocheck import { APIInterface, APIOptions } from "../"; +import { UserContext } from "../../../types"; export default function emailExists( apiImplementation: APIInterface, tenantId: string, options: APIOptions, - userContext: any + userContext: UserContext ): Promise; diff --git a/lib/build/recipe/emailpassword/api/generatePasswordResetToken.d.ts b/lib/build/recipe/emailpassword/api/generatePasswordResetToken.d.ts index b1d2f3116..146d43586 100644 --- a/lib/build/recipe/emailpassword/api/generatePasswordResetToken.d.ts +++ b/lib/build/recipe/emailpassword/api/generatePasswordResetToken.d.ts @@ -1,8 +1,9 @@ // @ts-nocheck import { APIInterface, APIOptions } from "../"; +import { UserContext } from "../../../types"; export default function generatePasswordResetToken( apiImplementation: APIInterface, tenantId: string, options: APIOptions, - userContext: any + userContext: UserContext ): Promise; diff --git a/lib/build/recipe/emailpassword/api/implementation.js b/lib/build/recipe/emailpassword/api/implementation.js index 284b26954..1897653f6 100644 --- a/lib/build/recipe/emailpassword/api/implementation.js +++ b/lib/build/recipe/emailpassword/api/implementation.js @@ -12,6 +12,7 @@ const recipe_1 = __importDefault(require("../../accountlinking/recipe")); const recipe_2 = __importDefault(require("../../emailverification/recipe")); const recipeUserId_1 = __importDefault(require("../../../recipeUserId")); const utils_1 = require("../utils"); +const recipe_3 = __importDefault(require("../../multifactorauth/recipe")); function getAPIImplementation() { return { emailExistsGET: async function ({ email, tenantId }) { @@ -475,25 +476,72 @@ function getAPIImplementation() { "Cannot sign in due to security reasons. Please try resetting your password, use a different login method or contact support. (ERR_CODE_008)", }; } - // the above sign in recipe function does not do account linking - so we do it here. - response.user = await recipe_1.default.getInstance().createPrimaryUserIdOrLinkAccounts({ + let session = await session_1.default.getSession(options.req, options.res, { + sessionRequired: false, + overrideGlobalClaimValidators: () => [], + }); + const mfaInstance = recipe_3.default.getInstance(); + // we do not want to attempt accountlinking when there is an active session and MFA is turned on + if (session === undefined || mfaInstance === undefined) { + // the above sign in recipe function does not do account linking - so we do it here. + response.user = await recipe_1.default.getInstance().createPrimaryUserIdOrLinkAccounts({ + tenantId, + user: response.user, + userContext, + }); + } + if (mfaInstance === undefined) { + // No MFA stuff here, so we just create and return the session + let session = await session_1.default.createNewOrKeepExistingSession( + options.req, + options.res, + tenantId, + emailPasswordRecipeUser.recipeUserId, + {}, + {}, + userContext + ); + return { + status: "OK", + session, + user: response.user, + }; + } + const mfaValidationRes = await mfaInstance.validateForMultifactorAuthBeforeFactorCompletion({ + req: options.req, + res: options.res, tenantId, - user: response.user, + factorIdInProgress: "emailpassword", + session, + userLoggingIn: response.user, + isAlreadySetup: true, userContext, }); - let session = await session_1.default.createNewSession( - options.req, - options.res, + if (mfaValidationRes.status === "FACTOR_SETUP_NOT_ALLOWED_ERROR") { + throw new Error("Should never come here"); + } + if (mfaValidationRes.status !== "OK") { + return mfaValidationRes; + } + const sessionRes = await mfaInstance.createOrUpdateSessionForMultifactorAuthAfterFactorCompletion({ + req: options.req, + res: options.res, tenantId, - emailPasswordRecipeUser.recipeUserId, - {}, - {}, - userContext - ); + factorIdInProgress: "emailpassword", + justCompletedFactorUserInfo: { + user: response.user, + createdNewUser: false, + recipeUserId: emailPasswordRecipeUser.recipeUserId, + }, + userContext, + }); + if (sessionRes.status !== "OK") { + return sessionRes; + } return { status: "OK", - session, - user: response.user, + session: sessionRes.session, + user: await __1.getUser(response.user.id, userContext), // fetching user again cause the user might have been updated while setting up mfa }; }, signUpPOST: async function ({ formFields, tenantId, options, userContext }) { @@ -541,11 +589,41 @@ function getAPIImplementation() { "Cannot sign up due to security reasons. Please try logging in, use a different login method or contact support. (ERR_CODE_007)", }; } + const mfaInstance = recipe_3.default.getInstance(); + if (mfaInstance !== undefined) { + let session = await session_1.default.getSession(options.req, options.res, { + sessionRequired: false, + overrideGlobalClaimValidators: () => [], + }); + const mfaValidationRes = await mfaInstance.validateForMultifactorAuthBeforeFactorCompletion({ + req: options.req, + res: options.res, + tenantId, + factorIdInProgress: "emailpassword", + session, + userLoggingIn: undefined, + isAlreadySetup: false, + signUpInfo: { + email, + isVerifiedFactor: false, + }, + userContext, + }); + if (mfaValidationRes.status !== "OK") { + return mfaValidationRes; + } + } + let session = session_1.default.getSession(options.req, options.res, { + sessionRequired: false, + overrideGlobalClaimValidators: () => [], + }); // this function also does account linking let response = await options.recipeImplementation.signUp({ tenantId, email, password, + // we do not want to attempt accountlinking when there is an active session and MFA is turned on + shouldAttemptAccountLinkingIfAllowed: session === undefined || mfaInstance === undefined, userContext, }); if (response.status === "EMAIL_ALREADY_EXISTS_ERROR") { @@ -558,18 +636,42 @@ function getAPIImplementation() { // this can happen cause of some race condition, but it's not a big deal. throw new Error("Race condition error - please call this API again"); } - let session = await session_1.default.createNewSession( - options.req, - options.res, + if (mfaInstance === undefined) { + // No MFA stuff here, so we just create and return the session + let session = await session_1.default.createNewOrKeepExistingSession( + options.req, + options.res, + tenantId, + emailPasswordRecipeUser.recipeUserId, + {}, + {}, + userContext + ); + return { + status: "OK", + session, + user: await __1.getUser(response.user.id, userContext), // fetching user again cause the user might have been updated while setting up mfa + }; + } + const sessionRes = await mfaInstance.createOrUpdateSessionForMultifactorAuthAfterFactorCompletion({ + req: options.req, + res: options.res, tenantId, - emailPasswordRecipeUser.recipeUserId, - {}, - {}, - userContext - ); + factorIdInProgress: "emailpassword", + isAlreadySetup: false, + justCompletedFactorUserInfo: { + user: response.user, + createdNewUser: true, + recipeUserId: emailPasswordRecipeUser.recipeUserId, + }, + userContext, + }); + if (sessionRes.status !== "OK") { + return sessionRes; + } return { status: "OK", - session, + session: sessionRes.session, user: response.user, }; }, diff --git a/lib/build/recipe/emailpassword/api/passwordReset.d.ts b/lib/build/recipe/emailpassword/api/passwordReset.d.ts index 3e1be7b68..08aef7f99 100644 --- a/lib/build/recipe/emailpassword/api/passwordReset.d.ts +++ b/lib/build/recipe/emailpassword/api/passwordReset.d.ts @@ -1,8 +1,9 @@ // @ts-nocheck import { APIInterface, APIOptions } from "../"; +import { UserContext } from "../../../types"; export default function passwordReset( apiImplementation: APIInterface, tenantId: string, options: APIOptions, - userContext: any + userContext: UserContext ): Promise; diff --git a/lib/build/recipe/emailpassword/api/signin.d.ts b/lib/build/recipe/emailpassword/api/signin.d.ts index 8ba114027..4a712efbe 100644 --- a/lib/build/recipe/emailpassword/api/signin.d.ts +++ b/lib/build/recipe/emailpassword/api/signin.d.ts @@ -1,8 +1,9 @@ // @ts-nocheck import { APIInterface, APIOptions } from "../"; +import { UserContext } from "../../../types"; export default function signInAPI( apiImplementation: APIInterface, tenantId: string, options: APIOptions, - userContext: any + userContext: UserContext ): Promise; diff --git a/lib/build/recipe/emailpassword/api/signup.d.ts b/lib/build/recipe/emailpassword/api/signup.d.ts index 0c799f379..1487513c1 100644 --- a/lib/build/recipe/emailpassword/api/signup.d.ts +++ b/lib/build/recipe/emailpassword/api/signup.d.ts @@ -1,8 +1,9 @@ // @ts-nocheck import { APIInterface, APIOptions } from "../"; +import { UserContext } from "../../../types"; export default function signUpAPI( apiImplementation: APIInterface, tenantId: string, options: APIOptions, - userContext: any + userContext: UserContext ): Promise; diff --git a/lib/build/recipe/emailpassword/emaildelivery/services/backwardCompatibility/index.d.ts b/lib/build/recipe/emailpassword/emaildelivery/services/backwardCompatibility/index.d.ts index 4ee0e54df..7184fcbbd 100644 --- a/lib/build/recipe/emailpassword/emaildelivery/services/backwardCompatibility/index.d.ts +++ b/lib/build/recipe/emailpassword/emaildelivery/services/backwardCompatibility/index.d.ts @@ -1,6 +1,6 @@ // @ts-nocheck import { TypeEmailPasswordEmailDeliveryInput } from "../../../types"; -import { NormalisedAppinfo } from "../../../../../types"; +import { NormalisedAppinfo, UserContext } from "../../../../../types"; import { EmailDeliveryInterface } from "../../../../../ingredients/emaildelivery/types"; export default class BackwardCompatibilityService implements EmailDeliveryInterface { @@ -9,7 +9,7 @@ export default class BackwardCompatibilityService constructor(appInfo: NormalisedAppinfo, isInServerlessEnv: boolean); sendEmail: ( input: TypeEmailPasswordEmailDeliveryInput & { - userContext: any; + userContext: UserContext; } ) => Promise; } diff --git a/lib/build/recipe/emailpassword/emaildelivery/services/smtp/index.d.ts b/lib/build/recipe/emailpassword/emaildelivery/services/smtp/index.d.ts index e17ccea3d..357c2f852 100644 --- a/lib/build/recipe/emailpassword/emaildelivery/services/smtp/index.d.ts +++ b/lib/build/recipe/emailpassword/emaildelivery/services/smtp/index.d.ts @@ -2,12 +2,13 @@ import { ServiceInterface, TypeInput } from "../../../../../ingredients/emaildelivery/services/smtp"; import { TypeEmailPasswordEmailDeliveryInput } from "../../../types"; import { EmailDeliveryInterface } from "../../../../../ingredients/emaildelivery/types"; +import { UserContext } from "../../../../../types"; export default class SMTPService implements EmailDeliveryInterface { serviceImpl: ServiceInterface; constructor(config: TypeInput); sendEmail: ( input: TypeEmailPasswordEmailDeliveryInput & { - userContext: any; + userContext: UserContext; } ) => Promise; } diff --git a/lib/build/recipe/emailpassword/index.d.ts b/lib/build/recipe/emailpassword/index.d.ts index 2061c43df..94245bd25 100644 --- a/lib/build/recipe/emailpassword/index.d.ts +++ b/lib/build/recipe/emailpassword/index.d.ts @@ -10,13 +10,13 @@ export default class Wrapper { tenantId: string, email: string, password: string, - userContext?: any + shouldAttemptAccountLinkingIfAllowed?: boolean, + userContext?: Record ): Promise< | { status: "OK"; user: import("../../types").User; recipeUserId: RecipeUserId; - isValidFirstFactorForTenant: boolean | undefined; } | { status: "EMAIL_ALREADY_EXISTS_ERROR"; @@ -26,13 +26,12 @@ export default class Wrapper { tenantId: string, email: string, password: string, - userContext?: any + userContext?: Record ): Promise< | { status: "OK"; user: import("../../types").User; recipeUserId: RecipeUserId; - isValidFirstFactorForTenant: boolean | undefined; } | { status: "WRONG_CREDENTIALS_ERROR"; @@ -53,7 +52,7 @@ export default class Wrapper { tenantId: string, userId: string, email: string, - userContext?: any + userContext?: Record ): Promise< | { status: "OK"; @@ -67,7 +66,7 @@ export default class Wrapper { tenantId: string, token: string, newPassword: string, - userContext?: any + userContext?: Record ): Promise< | { status: "RESET_PASSWORD_INVALID_TOKEN_ERROR"; @@ -87,7 +86,7 @@ export default class Wrapper { static consumePasswordResetToken( tenantId: string, token: string, - userContext?: any + userContext?: Record ): Promise< | { status: "OK"; @@ -102,7 +101,7 @@ export default class Wrapper { recipeUserId: RecipeUserId; email?: string; password?: string; - userContext?: any; + userContext?: Record; applyPasswordPolicy?: boolean; tenantIdForPasswordPolicy?: string; }): Promise< @@ -122,7 +121,7 @@ export default class Wrapper { tenantId: string, userId: string, email: string, - userContext?: any + userContext?: Record ): Promise< | { status: "OK"; @@ -136,13 +135,13 @@ export default class Wrapper { tenantId: string, userId: string, email: string, - userContext?: any + userContext?: Record ): Promise<{ status: "OK" | "UNKNOWN_USER_ID_ERROR"; }>; static sendEmail( input: TypeEmailPasswordEmailDeliveryInput & { - userContext?: any; + userContext?: Record; } ): Promise; } diff --git a/lib/build/recipe/emailpassword/index.js b/lib/build/recipe/emailpassword/index.js index 5c665fda5..eb686ae43 100644 --- a/lib/build/recipe/emailpassword/index.js +++ b/lib/build/recipe/emailpassword/index.js @@ -26,13 +26,15 @@ const recipeUserId_1 = __importDefault(require("../../recipeUserId")); const constants_1 = require("../multitenancy/constants"); const utils_1 = require("./utils"); const __1 = require("../.."); +const utils_2 = require("../../utils"); class Wrapper { - static signUp(tenantId, email, password, userContext) { + static signUp(tenantId, email, password, shouldAttemptAccountLinkingIfAllowed, userContext) { return recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.signUp({ email, password, tenantId: tenantId === undefined ? constants_1.DEFAULT_TENANT_ID : tenantId, - userContext: userContext === undefined ? {} : userContext, + shouldAttemptAccountLinkingIfAllowed, + userContext: utils_2.getUserContext(userContext), }); } static signIn(tenantId, email, password, userContext) { @@ -40,7 +42,7 @@ class Wrapper { email, password, tenantId: tenantId === undefined ? constants_1.DEFAULT_TENANT_ID : tenantId, - userContext: userContext === undefined ? {} : userContext, + userContext: utils_2.getUserContext(userContext), }); } /** @@ -59,7 +61,7 @@ class Wrapper { userId, email, tenantId: tenantId === undefined ? constants_1.DEFAULT_TENANT_ID : tenantId, - userContext: userContext === undefined ? {} : userContext, + userContext: utils_2.getUserContext(userContext), }); } static async resetPasswordUsingToken(tenantId, token, newPassword, userContext) { @@ -79,14 +81,13 @@ class Wrapper { return recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.consumePasswordResetToken({ token, tenantId: tenantId === undefined ? constants_1.DEFAULT_TENANT_ID : tenantId, - userContext: userContext === undefined ? {} : userContext, + userContext: utils_2.getUserContext(userContext), }); } static updateEmailOrPassword(input) { - var _a; return recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.updateEmailOrPassword( Object.assign(Object.assign({}, input), { - userContext: (_a = input.userContext) !== null && _a !== void 0 ? _a : {}, + userContext: utils_2.getUserContext(input.userContext), tenantIdForPasswordPolicy: input.tenantIdForPasswordPolicy === undefined ? constants_1.DEFAULT_TENANT_ID @@ -94,8 +95,9 @@ class Wrapper { }) ); } - static async createResetPasswordLink(tenantId, userId, email, userContext = {}) { - let token = await exports.createResetPasswordToken(tenantId, userId, email, userContext); + static async createResetPasswordLink(tenantId, userId, email, userContext) { + const ctx = utils_2.getUserContext(userContext); + let token = await exports.createResetPasswordToken(tenantId, userId, email, ctx); if (token.status === "UNKNOWN_USER_ID_ERROR") { return token; } @@ -107,12 +109,12 @@ class Wrapper { recipeId: recipeInstance.getRecipeId(), token: token.token, tenantId: tenantId === undefined ? constants_1.DEFAULT_TENANT_ID : tenantId, - request: __1.getRequestFromUserContext(userContext), - userContext, + request: __1.getRequestFromUserContext(ctx), + userContext: ctx, }), }; } - static async sendResetPasswordEmail(tenantId, userId, email, userContext = {}) { + static async sendResetPasswordEmail(tenantId, userId, email, userContext) { const user = await __1.getUser(userId, userContext); if (!user) { return { status: "UNKNOWN_USER_ID_ERROR" }; @@ -143,8 +145,9 @@ class Wrapper { static async sendEmail(input) { let recipeInstance = recipe_1.default.getInstanceOrThrowError(); return await recipeInstance.emailDelivery.ingredientInterfaceImpl.sendEmail( - Object.assign(Object.assign({ userContext: {} }, input), { + Object.assign(Object.assign({}, input), { tenantId: input.tenantId === undefined ? constants_1.DEFAULT_TENANT_ID : input.tenantId, + userContext: utils_2.getUserContext(input.userContext), }) ); } diff --git a/lib/build/recipe/emailpassword/recipe.d.ts b/lib/build/recipe/emailpassword/recipe.d.ts index d68aafab8..9ceb82533 100644 --- a/lib/build/recipe/emailpassword/recipe.d.ts +++ b/lib/build/recipe/emailpassword/recipe.d.ts @@ -1,7 +1,7 @@ // @ts-nocheck import RecipeModule from "../../recipeModule"; import { TypeInput, TypeNormalisedInput, RecipeInterface, APIInterface } from "./types"; -import { NormalisedAppinfo, APIHandled, HTTPMethod, RecipeListFunction } from "../../types"; +import { NormalisedAppinfo, APIHandled, HTTPMethod, RecipeListFunction, UserContext } from "../../types"; import STError from "./error"; import NormalisedURLPath from "../../normalisedURLPath"; import type { BaseRequest, BaseResponse } from "../../framework"; @@ -35,7 +35,7 @@ export default class Recipe extends RecipeModule { res: BaseResponse, _path: NormalisedURLPath, _method: HTTPMethod, - userContext: any + userContext: UserContext ) => Promise; handleError: (err: STError, _request: BaseRequest, response: BaseResponse) => Promise; getAllCORSHeaders: () => string[]; diff --git a/lib/build/recipe/emailpassword/recipe.js b/lib/build/recipe/emailpassword/recipe.js index 80e801b57..06bb02486 100644 --- a/lib/build/recipe/emailpassword/recipe.js +++ b/lib/build/recipe/emailpassword/recipe.js @@ -35,6 +35,8 @@ const implementation_1 = __importDefault(require("./api/implementation")); const querier_1 = require("../../querier"); const supertokens_js_override_1 = __importDefault(require("supertokens-js-override")); const emaildelivery_1 = __importDefault(require("../../ingredients/emaildelivery")); +const postSuperTokensInitCallbacks_1 = require("../../postSuperTokensInitCallbacks"); +const recipe_1 = __importDefault(require("../multifactorauth/recipe")); class Recipe extends recipeModule_1.default { constructor(recipeId, appInfo, isInServerlessEnv, config, ingredients) { super(recipeId, appInfo); @@ -156,6 +158,34 @@ class Recipe extends recipeModule_1.default { Recipe.instance = new Recipe(Recipe.RECIPE_ID, appInfo, isInServerlessEnv, config, { emailDelivery: undefined, }); + postSuperTokensInitCallbacks_1.PostSuperTokensInitCallbacks.addPostInitCallback(() => { + const mfaInstance = recipe_1.default.getInstance(); + if (mfaInstance !== undefined) { + mfaInstance.addGetAllFactorsFromOtherRecipesFunc((tenantConfig) => { + if (tenantConfig.passwordless.enabled === false) { + return { + factorIds: [], + firstFactorIds: [], + }; + } + return { + factorIds: ["emailpassword"], + firstFactorIds: ["emailpassword"], + }; + }); + mfaInstance.addGetFactorsSetupForUserFromOtherRecipes(async (user, tenantConfig) => { + if (tenantConfig.emailPassword.enabled === false) { + return []; + } + for (const loginMethod of user.loginMethods) { + if (loginMethod.recipeId === Recipe.RECIPE_ID) { + return ["emailpassword"]; + } + } + return []; + }); + } + }); return Recipe.instance; } else { throw new Error("Emailpassword recipe has already been initialised. Please check your code for bugs."); diff --git a/lib/build/recipe/emailpassword/recipeImplementation.js b/lib/build/recipe/emailpassword/recipeImplementation.js index 9d861d9bd..5f7073ffc 100644 --- a/lib/build/recipe/emailpassword/recipeImplementation.js +++ b/lib/build/recipe/emailpassword/recipeImplementation.js @@ -14,7 +14,7 @@ const constants_2 = require("../multitenancy/constants"); const user_1 = require("../../user"); function getRecipeInterface(querier, getEmailPasswordConfig) { return { - signUp: async function ({ email, password, tenantId, userContext }) { + signUp: async function ({ email, password, tenantId, shouldAttemptAccountLinkingIfAllowed, userContext }) { const response = await this.createNewRecipeUser({ email, password, @@ -24,16 +24,22 @@ function getRecipeInterface(querier, getEmailPasswordConfig) { if (response.status !== "OK") { return response; } - let updatedUser = await recipe_1.default.getInstance().createPrimaryUserIdOrLinkAccounts({ - tenantId, - user: response.user, - userContext, - }); + let updatedUser = response.user; + if ( + shouldAttemptAccountLinkingIfAllowed !== null && shouldAttemptAccountLinkingIfAllowed !== void 0 + ? shouldAttemptAccountLinkingIfAllowed + : true + ) { + updatedUser = await recipe_1.default.getInstance().createPrimaryUserIdOrLinkAccounts({ + tenantId, + user: response.user, + userContext, + }); + } return { status: "OK", user: updatedUser, recipeUserId: response.recipeUserId, - isValidFirstFactorForTenant: response.isValidFirstFactorForTenant, }; }, createNewRecipeUser: async function (input) { @@ -52,7 +58,6 @@ function getRecipeInterface(querier, getEmailPasswordConfig) { status: "OK", user: new user_1.User(resp.user), recipeUserId: new recipeUserId_1.default(resp.recipeUserId), - isValidFirstFactorForTenant: resp.isValidFirstFactorForTenant, }; } return resp; diff --git a/lib/build/recipe/emailpassword/types.d.ts b/lib/build/recipe/emailpassword/types.d.ts index faa4a37af..24fbae972 100644 --- a/lib/build/recipe/emailpassword/types.d.ts +++ b/lib/build/recipe/emailpassword/types.d.ts @@ -7,8 +7,9 @@ import { TypeInputWithService as EmailDeliveryTypeInputWithService, } from "../../ingredients/emaildelivery/types"; import EmailDeliveryIngredient from "../../ingredients/emaildelivery"; -import { GeneralErrorResponse, NormalisedAppinfo, User } from "../../types"; +import { GeneralErrorResponse, NormalisedAppinfo, User, UserContext } from "../../types"; import RecipeUserId from "../../recipeUserId"; +import { MFAFlowErrors } from "../multifactorauth/types"; export declare type TypeNormalisedInput = { signUpFeature: TypeNormalisedInputSignUp; signInFeature: TypeNormalisedInputSignIn; @@ -67,13 +68,13 @@ export declare type RecipeInterface = { email: string; password: string; tenantId: string; - userContext: any; + shouldAttemptAccountLinkingIfAllowed?: boolean; + userContext: UserContext; }): Promise< | { status: "OK"; user: User; recipeUserId: RecipeUserId; - isValidFirstFactorForTenant: boolean | undefined; } | { status: "EMAIL_ALREADY_EXISTS_ERROR"; @@ -83,13 +84,12 @@ export declare type RecipeInterface = { email: string; password: string; tenantId: string; - userContext: any; + userContext: UserContext; }): Promise< | { status: "OK"; user: User; recipeUserId: RecipeUserId; - isValidFirstFactorForTenant: boolean | undefined; } | { status: "EMAIL_ALREADY_EXISTS_ERROR"; @@ -99,13 +99,12 @@ export declare type RecipeInterface = { email: string; password: string; tenantId: string; - userContext: any; + userContext: UserContext; }): Promise< | { status: "OK"; user: User; recipeUserId: RecipeUserId; - isValidFirstFactorForTenant: boolean | undefined; } | { status: "WRONG_CREDENTIALS_ERROR"; @@ -120,7 +119,7 @@ export declare type RecipeInterface = { userId: string; email: string; tenantId: string; - userContext: any; + userContext: UserContext; }): Promise< | { status: "OK"; @@ -133,7 +132,7 @@ export declare type RecipeInterface = { consumePasswordResetToken(input: { token: string; tenantId: string; - userContext: any; + userContext: UserContext; }): Promise< | { status: "OK"; @@ -148,7 +147,7 @@ export declare type RecipeInterface = { recipeUserId: RecipeUserId; email?: string; password?: string; - userContext: any; + userContext: UserContext; applyPasswordPolicy?: boolean; tenantIdForPasswordPolicy: string; }): Promise< @@ -182,7 +181,7 @@ export declare type APIInterface = { email: string; tenantId: string; options: APIOptions; - userContext: any; + userContext: UserContext; }) => Promise< | { status: "OK"; @@ -199,7 +198,7 @@ export declare type APIInterface = { }[]; tenantId: string; options: APIOptions; - userContext: any; + userContext: UserContext; }) => Promise< | { status: "OK"; @@ -220,7 +219,7 @@ export declare type APIInterface = { token: string; tenantId: string; options: APIOptions; - userContext: any; + userContext: UserContext; }) => Promise< | { status: "OK"; @@ -245,7 +244,7 @@ export declare type APIInterface = { }[]; tenantId: string; options: APIOptions; - userContext: any; + userContext: UserContext; }) => Promise< | { status: "OK"; @@ -259,6 +258,7 @@ export declare type APIInterface = { | { status: "WRONG_CREDENTIALS_ERROR"; } + | MFAFlowErrors | GeneralErrorResponse >); signUpPOST: @@ -270,7 +270,7 @@ export declare type APIInterface = { }[]; tenantId: string; options: APIOptions; - userContext: any; + userContext: UserContext; }) => Promise< | { status: "OK"; @@ -284,6 +284,7 @@ export declare type APIInterface = { | { status: "EMAIL_ALREADY_EXISTS_ERROR"; } + | MFAFlowErrors | GeneralErrorResponse >); }; diff --git a/lib/build/recipe/emailpassword/utils.d.ts b/lib/build/recipe/emailpassword/utils.d.ts index f9ef8d5dc..3de0818e9 100644 --- a/lib/build/recipe/emailpassword/utils.d.ts +++ b/lib/build/recipe/emailpassword/utils.d.ts @@ -1,7 +1,7 @@ // @ts-nocheck import Recipe from "./recipe"; import { TypeInput, TypeNormalisedInput, NormalisedFormField, TypeInputFormField } from "./types"; -import { NormalisedAppinfo } from "../../types"; +import { NormalisedAppinfo, UserContext } from "../../types"; import { BaseRequest } from "../../framework"; export declare function validateAndNormaliseUserInput( recipeInstance: Recipe, @@ -28,5 +28,5 @@ export declare function getPasswordResetLink(input: { recipeId: string; tenantId: string; request: BaseRequest | undefined; - userContext: any; + userContext: UserContext; }): string; diff --git a/lib/build/recipe/emailverification/api/emailVerify.d.ts b/lib/build/recipe/emailverification/api/emailVerify.d.ts index 0edfa4a07..1f7b416c8 100644 --- a/lib/build/recipe/emailverification/api/emailVerify.d.ts +++ b/lib/build/recipe/emailverification/api/emailVerify.d.ts @@ -1,8 +1,9 @@ // @ts-nocheck import { APIInterface, APIOptions } from "../"; +import { UserContext } from "../../../types"; export default function emailVerify( apiImplementation: APIInterface, tenantId: string, options: APIOptions, - userContext: any + userContext: UserContext ): Promise; diff --git a/lib/build/recipe/emailverification/api/generateEmailVerifyToken.d.ts b/lib/build/recipe/emailverification/api/generateEmailVerifyToken.d.ts index d5c4bac05..487897d0e 100644 --- a/lib/build/recipe/emailverification/api/generateEmailVerifyToken.d.ts +++ b/lib/build/recipe/emailverification/api/generateEmailVerifyToken.d.ts @@ -1,7 +1,8 @@ // @ts-nocheck import { APIInterface, APIOptions } from "../"; +import { UserContext } from "../../../types"; export default function generateEmailVerifyToken( apiImplementation: APIInterface, options: APIOptions, - userContext: any + userContext: UserContext ): Promise; diff --git a/lib/build/recipe/emailverification/emailVerificationClaim.js b/lib/build/recipe/emailverification/emailVerificationClaim.js index 451c7c303..33da6f5fc 100644 --- a/lib/build/recipe/emailverification/emailVerificationClaim.js +++ b/lib/build/recipe/emailverification/emailVerificationClaim.js @@ -15,7 +15,7 @@ class EmailVerificationClaimClass extends claims_1.BooleanClaim { constructor() { super({ key: "st-ev", - async fetchValue(_userId, recipeUserId, __tenantId, userContext) { + async fetchValue(_userId, recipeUserId, __tenantId, _currentPayload, userContext) { const recipe = recipe_1.default.getInstanceOrThrowError(); let emailInfo = await recipe.getEmailForRecipeUserId(undefined, recipeUserId, userContext); if (emailInfo.status === "OK") { diff --git a/lib/build/recipe/emailverification/emaildelivery/services/backwardCompatibility/index.d.ts b/lib/build/recipe/emailverification/emaildelivery/services/backwardCompatibility/index.d.ts index e49c3c040..d5c7479ac 100644 --- a/lib/build/recipe/emailverification/emaildelivery/services/backwardCompatibility/index.d.ts +++ b/lib/build/recipe/emailverification/emaildelivery/services/backwardCompatibility/index.d.ts @@ -1,6 +1,6 @@ // @ts-nocheck import { TypeEmailVerificationEmailDeliveryInput } from "../../../types"; -import { NormalisedAppinfo } from "../../../../../types"; +import { NormalisedAppinfo, UserContext } from "../../../../../types"; import { EmailDeliveryInterface } from "../../../../../ingredients/emaildelivery/types"; export default class BackwardCompatibilityService implements EmailDeliveryInterface { @@ -9,7 +9,7 @@ export default class BackwardCompatibilityService constructor(appInfo: NormalisedAppinfo, isInServerlessEnv: boolean); sendEmail: ( input: TypeEmailVerificationEmailDeliveryInput & { - userContext: any; + userContext: UserContext; } ) => Promise; } diff --git a/lib/build/recipe/emailverification/emaildelivery/services/smtp/index.d.ts b/lib/build/recipe/emailverification/emaildelivery/services/smtp/index.d.ts index c3030da4e..cca4977ab 100644 --- a/lib/build/recipe/emailverification/emaildelivery/services/smtp/index.d.ts +++ b/lib/build/recipe/emailverification/emaildelivery/services/smtp/index.d.ts @@ -2,12 +2,13 @@ import { EmailDeliveryInterface } from "../../../../../ingredients/emaildelivery/types"; import { ServiceInterface, TypeInput } from "../../../../../ingredients/emaildelivery/services/smtp"; import { TypeEmailVerificationEmailDeliveryInput } from "../../../types"; +import { UserContext } from "../../../../../types"; export default class SMTPService implements EmailDeliveryInterface { serviceImpl: ServiceInterface; constructor(config: TypeInput); sendEmail: ( input: TypeEmailVerificationEmailDeliveryInput & { - userContext: any; + userContext: UserContext; } ) => Promise; } diff --git a/lib/build/recipe/emailverification/index.d.ts b/lib/build/recipe/emailverification/index.d.ts index fed350183..d502537b3 100644 --- a/lib/build/recipe/emailverification/index.d.ts +++ b/lib/build/recipe/emailverification/index.d.ts @@ -17,7 +17,7 @@ export default class Wrapper { tenantId: string, recipeUserId: RecipeUserId, email?: string, - userContext?: any + userContext?: Record ): Promise< | { status: "OK"; @@ -31,7 +31,7 @@ export default class Wrapper { tenantId: string, recipeUserId: RecipeUserId, email?: string, - userContext?: any + userContext?: Record ): Promise< | { status: "OK"; @@ -46,7 +46,7 @@ export default class Wrapper { userId: string, recipeUserId: RecipeUserId, email?: string, - userContext?: any + userContext?: Record ): Promise< | { status: "OK"; @@ -59,7 +59,7 @@ export default class Wrapper { tenantId: string, token: string, attemptAccountLinking?: boolean, - userContext?: any + userContext?: Record ): Promise< | { status: "OK"; @@ -69,25 +69,29 @@ export default class Wrapper { status: "EMAIL_VERIFICATION_INVALID_TOKEN_ERROR"; } >; - static isEmailVerified(recipeUserId: RecipeUserId, email?: string, userContext?: any): Promise; + static isEmailVerified( + recipeUserId: RecipeUserId, + email?: string, + userContext?: Record + ): Promise; static revokeEmailVerificationTokens( tenantId: string, recipeUserId: RecipeUserId, email?: string, - userContext?: any + userContext?: Record ): Promise<{ status: string; }>; static unverifyEmail( recipeUserId: RecipeUserId, email?: string, - userContext?: any + userContext?: Record ): Promise<{ status: string; }>; static sendEmail( input: TypeEmailVerificationEmailDeliveryInput & { - userContext?: any; + userContext?: Record; } ): Promise; } diff --git a/lib/build/recipe/emailverification/index.js b/lib/build/recipe/emailverification/index.js index 7651ef570..73c95ac5f 100644 --- a/lib/build/recipe/emailverification/index.js +++ b/lib/build/recipe/emailverification/index.js @@ -25,11 +25,13 @@ const error_1 = __importDefault(require("./error")); const emailVerificationClaim_1 = require("./emailVerificationClaim"); const utils_1 = require("./utils"); const __1 = require("../.."); +const utils_2 = require("../../utils"); class Wrapper { - static async createEmailVerificationToken(tenantId, recipeUserId, email, userContext = {}) { + static async createEmailVerificationToken(tenantId, recipeUserId, email, userContext) { + const ctx = utils_2.getUserContext(userContext); const recipeInstance = recipe_1.default.getInstanceOrThrowError(); if (email === undefined) { - const emailInfo = await recipeInstance.getEmailForRecipeUserId(undefined, recipeUserId, userContext); + const emailInfo = await recipeInstance.getEmailForRecipeUserId(undefined, recipeUserId, ctx); if (emailInfo.status === "OK") { email = emailInfo.email; } else if (emailInfo.status === "EMAIL_DOES_NOT_EXIST_ERROR") { @@ -44,18 +46,14 @@ class Wrapper { recipeUserId, email: email, tenantId, - userContext, + userContext: ctx, }); } - static async createEmailVerificationLink(tenantId, recipeUserId, email, userContext = {}) { + static async createEmailVerificationLink(tenantId, recipeUserId, email, userContext) { + const ctx = utils_2.getUserContext(userContext); const recipeInstance = recipe_1.default.getInstanceOrThrowError(); const appInfo = recipeInstance.getAppInfo(); - let emailVerificationToken = await exports.createEmailVerificationToken( - tenantId, - recipeUserId, - email, - userContext - ); + let emailVerificationToken = await exports.createEmailVerificationToken(tenantId, recipeUserId, email, ctx); if (emailVerificationToken.status === "EMAIL_ALREADY_VERIFIED_ERROR") { return { status: "EMAIL_ALREADY_VERIFIED_ERROR", @@ -68,15 +66,16 @@ class Wrapper { token: emailVerificationToken.token, recipeId: recipeInstance.getRecipeId(), tenantId, - request: __1.getRequestFromUserContext(userContext), - userContext, + request: __1.getRequestFromUserContext(ctx), + userContext: ctx, }), }; } - static async sendEmailVerificationEmail(tenantId, userId, recipeUserId, email, userContext = {}) { + static async sendEmailVerificationEmail(tenantId, userId, recipeUserId, email, userContext) { + const ctx = utils_2.getUserContext(userContext); if (email === undefined) { const recipeInstance = recipe_1.default.getInstanceOrThrowError(); - const emailInfo = await recipeInstance.getEmailForRecipeUserId(undefined, recipeUserId, userContext); + const emailInfo = await recipeInstance.getEmailForRecipeUserId(undefined, recipeUserId, ctx); if (emailInfo.status === "OK") { email = emailInfo.email; } else if (emailInfo.status === "EMAIL_DOES_NOT_EXIST_ERROR") { @@ -87,7 +86,7 @@ class Wrapper { throw new global.Error("Unknown User ID provided without email"); } } - let emailVerificationLink = await this.createEmailVerificationLink(tenantId, recipeUserId, email, userContext); + let emailVerificationLink = await this.createEmailVerificationLink(tenantId, recipeUserId, email, ctx); if (emailVerificationLink.status === "EMAIL_ALREADY_VERIFIED_ERROR") { return { status: "EMAIL_ALREADY_VERIFIED_ERROR", @@ -102,23 +101,25 @@ class Wrapper { }, emailVerifyLink: emailVerificationLink.link, tenantId, + userContext: ctx, }); return { status: "OK", }; } - static async verifyEmailUsingToken(tenantId, token, attemptAccountLinking = true, userContext = {}) { + static async verifyEmailUsingToken(tenantId, token, attemptAccountLinking = true, userContext) { return await recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.verifyEmailUsingToken({ token, tenantId, attemptAccountLinking, - userContext, + userContext: utils_2.getUserContext(userContext), }); } - static async isEmailVerified(recipeUserId, email, userContext = {}) { + static async isEmailVerified(recipeUserId, email, userContext) { + const ctx = utils_2.getUserContext(userContext); const recipeInstance = recipe_1.default.getInstanceOrThrowError(); if (email === undefined) { - const emailInfo = await recipeInstance.getEmailForRecipeUserId(undefined, recipeUserId, userContext); + const emailInfo = await recipeInstance.getEmailForRecipeUserId(undefined, recipeUserId, ctx); if (emailInfo.status === "OK") { email = emailInfo.email; } else if (emailInfo.status === "EMAIL_DOES_NOT_EXIST_ERROR") { @@ -130,16 +131,17 @@ class Wrapper { return await recipeInstance.recipeInterfaceImpl.isEmailVerified({ recipeUserId, email, - userContext, + userContext: ctx, }); } - static async revokeEmailVerificationTokens(tenantId, recipeUserId, email, userContext = {}) { + static async revokeEmailVerificationTokens(tenantId, recipeUserId, email, userContext) { + const ctx = utils_2.getUserContext(userContext); const recipeInstance = recipe_1.default.getInstanceOrThrowError(); // If the dev wants to delete the tokens for an old email address of the user they can pass the address // but redeeming those tokens would have no effect on isEmailVerified called without the old address // so in general that is not necessary either. if (email === undefined) { - const emailInfo = await recipeInstance.getEmailForRecipeUserId(undefined, recipeUserId, userContext); + const emailInfo = await recipeInstance.getEmailForRecipeUserId(undefined, recipeUserId, ctx); if (emailInfo.status === "OK") { email = emailInfo.email; } else if (emailInfo.status === "EMAIL_DOES_NOT_EXIST_ERROR") { @@ -157,13 +159,14 @@ class Wrapper { recipeUserId, email: email, tenantId, - userContext, + userContext: ctx, }); } - static async unverifyEmail(recipeUserId, email, userContext = {}) { + static async unverifyEmail(recipeUserId, email, userContext) { + const ctx = utils_2.getUserContext(userContext); const recipeInstance = recipe_1.default.getInstanceOrThrowError(); if (email === undefined) { - const emailInfo = await recipeInstance.getEmailForRecipeUserId(undefined, recipeUserId, userContext); + const emailInfo = await recipeInstance.getEmailForRecipeUserId(undefined, recipeUserId, ctx); if (emailInfo.status === "OK") { email = emailInfo.email; } else if (emailInfo.status === "EMAIL_DOES_NOT_EXIST_ERROR") { @@ -178,16 +181,13 @@ class Wrapper { return await recipeInstance.recipeInterfaceImpl.unverifyEmail({ recipeUserId, email, - userContext, + userContext: ctx, }); } static async sendEmail(input) { - var _a; let recipeInstance = recipe_1.default.getInstanceOrThrowError(); return await recipeInstance.emailDelivery.ingredientInterfaceImpl.sendEmail( - Object.assign(Object.assign({}, input), { - userContext: (_a = input.userContext) !== null && _a !== void 0 ? _a : {}, - }) + Object.assign(Object.assign({}, input), { userContext: utils_2.getUserContext(input.userContext) }) ); } } diff --git a/lib/build/recipe/emailverification/recipe.d.ts b/lib/build/recipe/emailverification/recipe.d.ts index 94132508c..fb0b8c778 100644 --- a/lib/build/recipe/emailverification/recipe.d.ts +++ b/lib/build/recipe/emailverification/recipe.d.ts @@ -1,7 +1,7 @@ // @ts-nocheck import RecipeModule from "../../recipeModule"; import { TypeInput, TypeNormalisedInput, RecipeInterface, APIInterface, GetEmailForRecipeUserIdFunc } from "./types"; -import { NormalisedAppinfo, APIHandled, RecipeListFunction, HTTPMethod } from "../../types"; +import { NormalisedAppinfo, APIHandled, RecipeListFunction, HTTPMethod, UserContext } from "../../types"; import STError from "./error"; import NormalisedURLPath from "../../normalisedURLPath"; import type { BaseRequest, BaseResponse } from "../../framework"; @@ -38,18 +38,18 @@ export default class Recipe extends RecipeModule { res: BaseResponse, _: NormalisedURLPath, __: HTTPMethod, - userContext: any + userContext: UserContext ) => Promise; handleError: (err: STError, _: BaseRequest, __: BaseResponse) => Promise; getAllCORSHeaders: () => string[]; isErrorFromThisRecipe: (err: any) => err is STError; getEmailForRecipeUserId: GetEmailForRecipeUserIdFunc; - getPrimaryUserIdForRecipeUser: (recipeUserId: RecipeUserId, userContext: any) => Promise; + getPrimaryUserIdForRecipeUser: (recipeUserId: RecipeUserId, userContext: UserContext) => Promise; updateSessionIfRequiredPostEmailVerification: (input: { req: BaseRequest; res: BaseResponse; session: SessionContainerInterface | undefined; recipeUserIdWhoseEmailGotVerified: RecipeUserId; - userContext: any; + userContext: UserContext; }) => Promise; } diff --git a/lib/build/recipe/emailverification/types.d.ts b/lib/build/recipe/emailverification/types.d.ts index 0c7aa6824..1877b1752 100644 --- a/lib/build/recipe/emailverification/types.d.ts +++ b/lib/build/recipe/emailverification/types.d.ts @@ -6,7 +6,7 @@ import { TypeInputWithService as EmailDeliveryTypeInputWithService, } from "../../ingredients/emaildelivery/types"; import EmailDeliveryIngredient from "../../ingredients/emaildelivery"; -import { GeneralErrorResponse, NormalisedAppinfo } from "../../types"; +import { GeneralErrorResponse, NormalisedAppinfo, UserContext } from "../../types"; import { SessionContainerInterface } from "../session/types"; import RecipeUserId from "../../recipeUserId"; import { User } from "../../types"; @@ -15,7 +15,7 @@ export declare type TypeInput = { emailDelivery?: EmailDeliveryTypeInput; getEmailForRecipeUserId?: ( recipeUserId: RecipeUserId, - userContext: any + userContext: UserContext ) => Promise< | { status: "OK"; @@ -40,7 +40,7 @@ export declare type TypeNormalisedInput = { ) => EmailDeliveryTypeInputWithService; getEmailForRecipeUserId?: ( recipeUserId: RecipeUserId, - userContext: any + userContext: UserContext ) => Promise< | { status: "OK"; @@ -67,7 +67,7 @@ export declare type RecipeInterface = { recipeUserId: RecipeUserId; email: string; tenantId: string; - userContext: any; + userContext: UserContext; }): Promise< | { status: "OK"; @@ -81,7 +81,7 @@ export declare type RecipeInterface = { token: string; attemptAccountLinking: boolean; tenantId: string; - userContext: any; + userContext: UserContext; }): Promise< | { status: "OK"; @@ -91,19 +91,19 @@ export declare type RecipeInterface = { status: "EMAIL_VERIFICATION_INVALID_TOKEN_ERROR"; } >; - isEmailVerified(input: { recipeUserId: RecipeUserId; email: string; userContext: any }): Promise; + isEmailVerified(input: { recipeUserId: RecipeUserId; email: string; userContext: UserContext }): Promise; revokeEmailVerificationTokens(input: { recipeUserId: RecipeUserId; email: string; tenantId: string; - userContext: any; + userContext: UserContext; }): Promise<{ status: "OK"; }>; unverifyEmail(input: { recipeUserId: RecipeUserId; email: string; - userContext: any; + userContext: UserContext; }): Promise<{ status: "OK"; }>; @@ -125,7 +125,7 @@ export declare type APIInterface = { token: string; tenantId: string; options: APIOptions; - userContext: any; + userContext: UserContext; session?: SessionContainerInterface; }) => Promise< | { @@ -142,7 +142,7 @@ export declare type APIInterface = { | undefined | ((input: { options: APIOptions; - userContext: any; + userContext: UserContext; session: SessionContainerInterface; }) => Promise< | { @@ -156,7 +156,7 @@ export declare type APIInterface = { | undefined | ((input: { options: APIOptions; - userContext: any; + userContext: UserContext; session: SessionContainerInterface; }) => Promise< | { @@ -182,7 +182,7 @@ export declare type TypeEmailVerificationEmailDeliveryInput = { export declare type GetEmailForRecipeUserIdFunc = ( user: User | undefined, recipeUserId: RecipeUserId, - userContext: any + userContext: UserContext ) => Promise< | { status: "OK"; diff --git a/lib/build/recipe/emailverification/utils.d.ts b/lib/build/recipe/emailverification/utils.d.ts index 87217577f..6f6919d38 100644 --- a/lib/build/recipe/emailverification/utils.d.ts +++ b/lib/build/recipe/emailverification/utils.d.ts @@ -1,7 +1,7 @@ // @ts-nocheck import Recipe from "./recipe"; import { TypeInput, TypeNormalisedInput } from "./types"; -import { NormalisedAppinfo } from "../../types"; +import { NormalisedAppinfo, UserContext } from "../../types"; import { BaseRequest } from "../../framework"; export declare function validateAndNormaliseUserInput( _: Recipe, @@ -14,5 +14,5 @@ export declare function getEmailVerifyLink(input: { recipeId: string; tenantId: string; request: BaseRequest | undefined; - userContext: any; + userContext: UserContext; }): string; diff --git a/lib/build/recipe/jwt/api/getJWKS.d.ts b/lib/build/recipe/jwt/api/getJWKS.d.ts index a99c73e0d..92997a1a4 100644 --- a/lib/build/recipe/jwt/api/getJWKS.d.ts +++ b/lib/build/recipe/jwt/api/getJWKS.d.ts @@ -1,7 +1,8 @@ // @ts-nocheck +import { UserContext } from "../../../types"; import { APIInterface, APIOptions } from "../types"; export default function getJWKS( apiImplementation: APIInterface, options: APIOptions, - userContext: any + userContext: UserContext ): Promise; diff --git a/lib/build/recipe/jwt/index.d.ts b/lib/build/recipe/jwt/index.d.ts index 0145bdc5b..ed6ab4c4e 100644 --- a/lib/build/recipe/jwt/index.d.ts +++ b/lib/build/recipe/jwt/index.d.ts @@ -7,7 +7,7 @@ export default class Wrapper { payload: any, validitySeconds?: number, useStaticSigningKey?: boolean, - userContext?: any + userContext?: Record ): Promise< | { status: "OK"; @@ -18,7 +18,7 @@ export default class Wrapper { } >; static getJWKS( - userContext?: any + userContext?: Record ): Promise<{ keys: JsonWebKey[]; validityInSeconds?: number | undefined; diff --git a/lib/build/recipe/jwt/index.js b/lib/build/recipe/jwt/index.js index 646e50f44..8ba933f81 100644 --- a/lib/build/recipe/jwt/index.js +++ b/lib/build/recipe/jwt/index.js @@ -20,6 +20,7 @@ var __importDefault = }; Object.defineProperty(exports, "__esModule", { value: true }); exports.getJWKS = exports.createJWT = exports.init = void 0; +const utils_1 = require("../../utils"); const recipe_1 = __importDefault(require("./recipe")); class Wrapper { static async createJWT(payload, validitySeconds, useStaticSigningKey, userContext) { @@ -27,12 +28,12 @@ class Wrapper { payload, validitySeconds, useStaticSigningKey, - userContext: userContext === undefined ? {} : userContext, + userContext: utils_1.getUserContext(userContext), }); } static async getJWKS(userContext) { return await recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.getJWKS({ - userContext: userContext === undefined ? {} : userContext, + userContext: utils_1.getUserContext(userContext), }); } } diff --git a/lib/build/recipe/jwt/recipe.d.ts b/lib/build/recipe/jwt/recipe.d.ts index f15875f2a..076cc7c64 100644 --- a/lib/build/recipe/jwt/recipe.d.ts +++ b/lib/build/recipe/jwt/recipe.d.ts @@ -3,7 +3,7 @@ import error from "../../error"; import type { BaseRequest, BaseResponse } from "../../framework"; import normalisedURLPath from "../../normalisedURLPath"; import RecipeModule from "../../recipeModule"; -import { APIHandled, HTTPMethod, NormalisedAppinfo, RecipeListFunction } from "../../types"; +import { APIHandled, HTTPMethod, NormalisedAppinfo, RecipeListFunction, UserContext } from "../../types"; import { APIInterface, RecipeInterface, TypeInput, TypeNormalisedInput } from "./types"; export default class Recipe extends RecipeModule { static RECIPE_ID: string; @@ -24,9 +24,9 @@ export default class Recipe extends RecipeModule { res: BaseResponse, _path: normalisedURLPath, _method: HTTPMethod, - userContext: any + userContext: UserContext ) => Promise; - handleError(error: error, _: BaseRequest, __: BaseResponse): Promise; + handleError(error: error, _: BaseRequest, __: BaseResponse, _userContext: UserContext): Promise; getAllCORSHeaders(): string[]; isErrorFromThisRecipe(err: any): err is error; } diff --git a/lib/build/recipe/jwt/recipe.js b/lib/build/recipe/jwt/recipe.js index 2b1b48575..8b1594f86 100644 --- a/lib/build/recipe/jwt/recipe.js +++ b/lib/build/recipe/jwt/recipe.js @@ -94,7 +94,7 @@ class Recipe extends recipeModule_1.default { }, ]; } - handleError(error, _, __) { + handleError(error, _, __, _userContext) { throw error; } getAllCORSHeaders() { diff --git a/lib/build/recipe/jwt/recipeImplementation.js b/lib/build/recipe/jwt/recipeImplementation.js index ef0f75a4f..073f14b88 100644 --- a/lib/build/recipe/jwt/recipeImplementation.js +++ b/lib/build/recipe/jwt/recipeImplementation.js @@ -50,7 +50,7 @@ function getRecipeInterface(querier, config, appInfo) { }; } }, - getJWKS: async function (userContext) { + getJWKS: async function ({ userContext }) { const { body, headers } = await querier.sendGetRequestWithResponseHeaders( new normalisedURLPath_1.default("/.well-known/jwks.json"), {}, diff --git a/lib/build/recipe/jwt/types.d.ts b/lib/build/recipe/jwt/types.d.ts index 337815a43..84c1802ae 100644 --- a/lib/build/recipe/jwt/types.d.ts +++ b/lib/build/recipe/jwt/types.d.ts @@ -1,7 +1,7 @@ // @ts-nocheck import type { BaseRequest, BaseResponse } from "../../framework"; import OverrideableBuilder from "supertokens-js-override"; -import { GeneralErrorResponse } from "../../types"; +import { GeneralErrorResponse, UserContext } from "../../types"; export declare type JsonWebKey = { kty: string; kid: string; @@ -43,7 +43,7 @@ export declare type RecipeInterface = { payload?: any; validitySeconds?: number; useStaticSigningKey?: boolean; - userContext: any; + userContext: UserContext; }): Promise< | { status: "OK"; @@ -54,7 +54,7 @@ export declare type RecipeInterface = { } >; getJWKS(input: { - userContext: any; + userContext: UserContext; }): Promise<{ keys: JsonWebKey[]; validityInSeconds?: number; @@ -65,7 +65,7 @@ export declare type APIInterface = { | undefined | ((input: { options: APIOptions; - userContext: any; + userContext: UserContext; }) => Promise< | { keys: JsonWebKey[]; diff --git a/lib/build/recipe/multifactorauth/api/implementation.js b/lib/build/recipe/multifactorauth/api/implementation.js index 31243784c..730fd097e 100644 --- a/lib/build/recipe/multifactorauth/api/implementation.js +++ b/lib/build/recipe/multifactorauth/api/implementation.js @@ -1,6 +1,125 @@ "use strict"; +var __rest = + (this && this.__rest) || + function (s, e) { + var t = {}; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0) t[p] = s[p]; + if (s != null && typeof Object.getOwnPropertySymbols === "function") + for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) { + if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i])) t[p[i]] = s[p[i]]; + } + return t; + }; +var __importDefault = + (this && this.__importDefault) || + function (mod) { + return mod && mod.__esModule ? mod : { default: mod }; + }; Object.defineProperty(exports, "__esModule", { value: true }); +const multitenancy_1 = __importDefault(require("../../multitenancy")); +const multiFactorAuthClaim_1 = require("../multiFactorAuthClaim"); +const __1 = require("../../.."); +const error_1 = __importDefault(require("../../session/error")); function getAPIInterface() { - return {}; + return { + mfaInfoGET: async ({ options, session, userContext }) => { + var _a, _b, _c; + const userId = session.getUserId(); + const tenantId = session.getTenantId(); + const user = await __1.getUser(userId, userContext); + if (user === undefined) { + throw new error_1.default({ + type: error_1.default.UNAUTHORISED, + message: "Session user not found", + }); + } + const tenantInfo = await multitenancy_1.default.getTenant(tenantId, userContext); + const isAlreadySetup = await options.recipeImplementation.getFactorsSetupForUser({ + tenantId, + user, + userContext, + }); + const _d = tenantInfo, + { status: _ } = _d, + tenantConfig = __rest(_d, ["status"]); + const availableFactors = await options.recipeInstance.getAllAvailableFactorIds(tenantConfig); + // session is active and a new user is going to be created, so we need to check if the factor setup is allowed + const defaultRequiredFactorIdsForUser = await options.recipeImplementation.getDefaultRequiredFactorsForUser( + { + user: user, + tenantId, + userContext, + } + ); + const completedFactorsClaimValue = await session.getClaimValue( + multiFactorAuthClaim_1.MultiFactorAuthClaim, + userContext + ); + const completedFactors = + (_a = + completedFactorsClaimValue === null || completedFactorsClaimValue === void 0 + ? void 0 + : completedFactorsClaimValue.c) !== null && _a !== void 0 + ? _a + : {}; + const mfaRequirementsForAuth = await options.recipeImplementation.getMFARequirementsForAuth({ + user: user, + accessTokenPayload: session.getAccessTokenPayload(), + tenantId, + factorsSetUpForUser: isAlreadySetup, + defaultRequiredFactorIdsForTenant: + (_b = + tenantInfo === null || tenantInfo === void 0 ? void 0 : tenantInfo.defaultRequiredFactorIds) !== + null && _b !== void 0 + ? _b + : [], + defaultRequiredFactorIdsForUser, + completedFactors: completedFactors, + userContext, + }); + const isAllowedToSetup = []; + for (const id of availableFactors) { + if ( + await options.recipeImplementation.isAllowedToSetupFactor({ + session, + factorId: id, + completedFactors: completedFactors, + defaultRequiredFactorIdsForTenant: + (_c = + tenantInfo === null || tenantInfo === void 0 + ? void 0 + : tenantInfo.defaultRequiredFactorIds) !== null && _c !== void 0 + ? _c + : [], + defaultRequiredFactorIdsForUser, + factorsSetUpForUser: isAlreadySetup, + mfaRequirementsForAuth, + userContext, + }) + ) { + isAllowedToSetup.push(id); + } + } + let selectedEmail = user.emails[0]; + for (const loginMethod of user.loginMethods) { + if (loginMethod.recipeUserId.getAsString() === session.getRecipeUserId().getAsString()) { + if (loginMethod.email !== undefined) { + selectedEmail = loginMethod.email; + } + break; + } + } + await session.fetchAndSetClaim(multiFactorAuthClaim_1.MultiFactorAuthClaim, userContext); + return { + status: "OK", + factors: { + isAllowedToSetup, + isAlreadySetup, + }, + email: selectedEmail, + phoneNumber: user.phoneNumbers[0], + }; + }, + }; } exports.default = getAPIInterface; diff --git a/lib/build/recipe/multifactorauth/api/mfaInfo.d.ts b/lib/build/recipe/multifactorauth/api/mfaInfo.d.ts index d0751ff81..8cb7c2adb 100644 --- a/lib/build/recipe/multifactorauth/api/mfaInfo.d.ts +++ b/lib/build/recipe/multifactorauth/api/mfaInfo.d.ts @@ -1,7 +1,8 @@ // @ts-nocheck import { APIInterface, APIOptions } from ".."; +import { UserContext } from "../../../types"; export default function mfaInfo( apiImplementation: APIInterface, options: APIOptions, - userContext: any + userContext: UserContext ): Promise; diff --git a/lib/build/recipe/multifactorauth/api/mfaInfo.js b/lib/build/recipe/multifactorauth/api/mfaInfo.js index 6f0ca3f80..c2ab0d246 100644 --- a/lib/build/recipe/multifactorauth/api/mfaInfo.js +++ b/lib/build/recipe/multifactorauth/api/mfaInfo.js @@ -22,7 +22,6 @@ Object.defineProperty(exports, "__esModule", { value: true }); const utils_1 = require("../../../utils"); const session_1 = __importDefault(require("../../session")); async function mfaInfo(apiImplementation, options, userContext) { - let result; if (apiImplementation.mfaInfoGET === undefined) { return false; } @@ -37,15 +36,7 @@ async function mfaInfo(apiImplementation, options, userContext) { session, userContext, }); - if (response.status === "OK") { - // if there is a new session, it will be - // automatically added to the response by the createNewSession function call - // inside the verifyEmailPOST function. - result = { status: "OK" }; - } else { - result = response; - } - utils_1.send200Response(options.res, result); + utils_1.send200Response(options.res, response); return true; } exports.default = mfaInfo; diff --git a/lib/build/recipe/multifactorauth/constants.d.ts b/lib/build/recipe/multifactorauth/constants.d.ts index 05e62e67d..51794abd1 100644 --- a/lib/build/recipe/multifactorauth/constants.d.ts +++ b/lib/build/recipe/multifactorauth/constants.d.ts @@ -1,2 +1,2 @@ // @ts-nocheck -export declare const GET_MFA_INFO = "/mfa-info"; +export declare const GET_MFA_INFO = "/mfa/info"; diff --git a/lib/build/recipe/multifactorauth/constants.js b/lib/build/recipe/multifactorauth/constants.js index 14dad1d2f..8ee9d3361 100644 --- a/lib/build/recipe/multifactorauth/constants.js +++ b/lib/build/recipe/multifactorauth/constants.js @@ -15,4 +15,4 @@ */ Object.defineProperty(exports, "__esModule", { value: true }); exports.GET_MFA_INFO = void 0; -exports.GET_MFA_INFO = "/mfa-info"; +exports.GET_MFA_INFO = "/mfa/info"; diff --git a/lib/build/recipe/multifactorauth/error.d.ts b/lib/build/recipe/multifactorauth/error.d.ts deleted file mode 100644 index 486758b61..000000000 --- a/lib/build/recipe/multifactorauth/error.d.ts +++ /dev/null @@ -1,5 +0,0 @@ -// @ts-nocheck -import STError from "../../error"; -export default class SessionError extends STError { - constructor(options: { type: "BAD_INPUT_ERROR"; message: string }); -} diff --git a/lib/build/recipe/multifactorauth/error.js b/lib/build/recipe/multifactorauth/error.js deleted file mode 100644 index 9ceda466f..000000000 --- a/lib/build/recipe/multifactorauth/error.js +++ /dev/null @@ -1,29 +0,0 @@ -"use strict"; -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -var __importDefault = - (this && this.__importDefault) || - function (mod) { - return mod && mod.__esModule ? mod : { default: mod }; - }; -Object.defineProperty(exports, "__esModule", { value: true }); -const error_1 = __importDefault(require("../../error")); -class SessionError extends error_1.default { - constructor(options) { - super(Object.assign({}, options)); - this.fromRecipe = "multifactorauth"; - } -} -exports.default = SessionError; diff --git a/lib/build/recipe/multifactorauth/index.d.ts b/lib/build/recipe/multifactorauth/index.d.ts index 8a79150be..765e8d470 100644 --- a/lib/build/recipe/multifactorauth/index.d.ts +++ b/lib/build/recipe/multifactorauth/index.d.ts @@ -6,19 +6,31 @@ import { SessionContainerInterface } from "../session/types"; export default class Wrapper { static init: typeof Recipe.init; static MultiFactorAuthClaim: import("./multiFactorAuthClaim").MultiFactorAuthClaimClass; - static getFactorsSetUpByUser(tenantId: string, userId: string, userContext?: any): Promise; + static getFactorsSetupForUser( + tenantId: string, + userId: string, + userContext?: Record + ): Promise; static isAllowedToSetupFactor( session: SessionContainerInterface, factorId: string, - userContext?: any + userContext?: Record ): Promise; static markFactorAsCompleteInSession( session: SessionContainerInterface, factorId: string, - userContext?: any + userContext?: Record + ): Promise; + static addToDefaultRequiredFactorsForUser( + userId: string, + factorId: string, + userContext?: Record ): Promise; } export declare let init: typeof Recipe.init; +export declare let getFactorsSetupForUser: typeof Wrapper.getFactorsSetupForUser; +export declare let isAllowedToSetupFactor: typeof Wrapper.isAllowedToSetupFactor; export declare let markFactorAsCompleteInSession: typeof Wrapper.markFactorAsCompleteInSession; +export declare const addToDefaultRequiredFactorsForUser: typeof Wrapper.addToDefaultRequiredFactorsForUser; export { MultiFactorAuthClaim }; export type { RecipeInterface, APIOptions, APIInterface }; diff --git a/lib/build/recipe/multifactorauth/index.js b/lib/build/recipe/multifactorauth/index.js index 678298851..7f089fa7e 100644 --- a/lib/build/recipe/multifactorauth/index.js +++ b/lib/build/recipe/multifactorauth/index.js @@ -19,7 +19,7 @@ var __importDefault = return mod && mod.__esModule ? mod : { default: mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); -exports.MultiFactorAuthClaim = exports.markFactorAsCompleteInSession = exports.init = void 0; +exports.MultiFactorAuthClaim = exports.addToDefaultRequiredFactorsForUser = exports.markFactorAsCompleteInSession = exports.isAllowedToSetupFactor = exports.getFactorsSetupForUser = exports.init = void 0; const recipe_1 = __importDefault(require("./recipe")); const multiFactorAuthClaim_1 = require("./multiFactorAuthClaim"); Object.defineProperty(exports, "MultiFactorAuthClaim", { @@ -28,23 +28,26 @@ Object.defineProperty(exports, "MultiFactorAuthClaim", { return multiFactorAuthClaim_1.MultiFactorAuthClaim; }, }); +const multitenancy_1 = __importDefault(require("../multitenancy")); const __1 = require("../.."); +const recipe_2 = __importDefault(require("../usermetadata/recipe")); +const utils_1 = require("../../utils"); class Wrapper { - static async getFactorsSetUpByUser(tenantId, userId, userContext) { - const ctx = userContext !== null && userContext !== void 0 ? userContext : {}; + static async getFactorsSetupForUser(tenantId, userId, userContext) { + const ctx = utils_1.getUserContext(userContext); const user = await __1.getUser(userId, ctx); if (!user) { throw new Error("UKNKNOWN_USER_ID"); } return recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.getFactorsSetupForUser({ - user, tenantId, + user, userContext: ctx, }); } static async isAllowedToSetupFactor(session, factorId, userContext) { - var _a; - let ctx = userContext !== null && userContext !== void 0 ? userContext : {}; + var _a, _b; + let ctx = utils_1.getUserContext(userContext); const user = await __1.getUser(session.getUserId(), ctx); if (!user) { throw new Error("UKNKNOWN_USER_ID"); @@ -62,13 +65,26 @@ class Wrapper { _a !== void 0 ? _a : {}; - const defaultMFARequirementsForUser = []; // TODO - const defaultMFARequirementsForTenant = []; // TODO + const defaultMFARequirementsForUser = await recipe_1.default + .getInstanceOrThrowError() + .recipeInterfaceImpl.getDefaultRequiredFactorsForUser({ + user, + tenantId: session.getTenantId(), + userContext: ctx, + }); + const tenantInfo = await multitenancy_1.default.getTenant(session.getTenantId(), userContext); + const defaultMFARequirementsForTenant = + (_b = tenantInfo === null || tenantInfo === void 0 ? void 0 : tenantInfo.defaultRequiredFactorIds) !== + null && _b !== void 0 + ? _b + : []; const requirements = await recipe_1.default .getInstanceOrThrowError() .recipeInterfaceImpl.getMFARequirementsForAuth({ - session, - factorsSetUpByTheUser: factorsSetup, + user, + accessTokenPayload: session.getAccessTokenPayload(), + tenantId: session.getTenantId(), + factorsSetUpForUser: factorsSetup, defaultRequiredFactorIdsForUser: defaultMFARequirementsForUser, defaultRequiredFactorIdsForTenant: defaultMFARequirementsForTenant, completedFactors, @@ -79,17 +95,47 @@ class Wrapper { factorId, completedFactors, mfaRequirementsForAuth: requirements, - factorsSetUpByTheUser: factorsSetup, + factorsSetUpForUser: factorsSetup, defaultRequiredFactorIdsForUser: defaultMFARequirementsForUser, defaultRequiredFactorIdsForTenant: defaultMFARequirementsForTenant, - userContext, + userContext: ctx, }); } static async markFactorAsCompleteInSession(session, factorId, userContext) { return recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.markFactorAsCompleteInSession({ session, factorId, - userContext: userContext !== null && userContext !== void 0 ? userContext : {}, + userContext: utils_1.getUserContext(userContext), + }); + } + static async addToDefaultRequiredFactorsForUser(userId, factorId, userContext) { + var _a, _b; + const ctx = utils_1.getUserContext(userContext); + const userMetadataInstance = recipe_2.default.getInstanceOrThrowError(); + const metadata = await userMetadataInstance.recipeInterfaceImpl.getUserMetadata({ + userId, + userContext: ctx, + }); + const factorIds = + (_b = + (_a = metadata.metadata._supertokens) === null || _a === void 0 + ? void 0 + : _a.defaultRequiredFactorIdsForUser) !== null && _b !== void 0 + ? _b + : []; + if (factorIds.includes(factorId)) { + return; + } + factorIds.push(factorId); + const metadataUpdate = Object.assign(Object.assign({}, metadata.metadata), { + _supertokens: Object.assign(Object.assign({}, metadata.metadata._supertokens), { + defaultRequiredFactorIdsForUser: factorIds, + }), + }); + await userMetadataInstance.recipeInterfaceImpl.updateUserMetadataInternal({ + userId: userId, + metadataUpdate, + userContext: ctx, }); } } @@ -97,4 +143,7 @@ exports.default = Wrapper; Wrapper.init = recipe_1.default.init; Wrapper.MultiFactorAuthClaim = multiFactorAuthClaim_1.MultiFactorAuthClaim; exports.init = Wrapper.init; +exports.getFactorsSetupForUser = Wrapper.getFactorsSetupForUser; +exports.isAllowedToSetupFactor = Wrapper.isAllowedToSetupFactor; exports.markFactorAsCompleteInSession = Wrapper.markFactorAsCompleteInSession; +exports.addToDefaultRequiredFactorsForUser = Wrapper.addToDefaultRequiredFactorsForUser; diff --git a/lib/build/recipe/multifactorauth/multiFactorAuthClaim.d.ts b/lib/build/recipe/multifactorauth/multiFactorAuthClaim.d.ts index b61fbca23..41a2a5baf 100644 --- a/lib/build/recipe/multifactorauth/multiFactorAuthClaim.d.ts +++ b/lib/build/recipe/multifactorauth/multiFactorAuthClaim.d.ts @@ -4,24 +4,27 @@ import { SessionClaimValidator } from "../session"; import { SessionClaim } from "../session/claims"; import { JSONObject } from "../usermetadata"; import { MFAClaimValue, MFARequirementList } from "./types"; +import { UserContext } from "../../types"; /** * We include "Class" in the class name, because it makes it easier to import the right thing (the instance) instead of this. * */ export declare class MultiFactorAuthClaimClass extends SessionClaim { + constructor(key?: string); validators: { - passesMFARequirements: (requirements?: MFARequirementList) => SessionClaimValidator; + hasCompletedDefaultFactors: (id?: string) => SessionClaimValidator; + hasCompletedFactors(requirements: MFARequirementList, id?: string): SessionClaimValidator; }; - constructor(key?: string); - buildNextArray(_completedClaims: MFAClaimValue["c"], _requirements: MFARequirementList): never[]; + buildNextArray(completedClaims: MFAClaimValue["c"], requirements: MFARequirementList): string[]; fetchValue: ( - _userId: string, + userId: string, _recipeUserId: RecipeUserId, - _tenantId: string | undefined, - _userContext: any - ) => { - c: {}; - n: never[]; - }; + tenantId: string | undefined, + currentPayload: JSONObject | undefined, + userContext: UserContext + ) => Promise<{ + c: Record; + n: string[]; + }>; addToPayload_internal: ( payload: JSONObject, value: MFAClaimValue diff --git a/lib/build/recipe/multifactorauth/multiFactorAuthClaim.js b/lib/build/recipe/multifactorauth/multiFactorAuthClaim.js index 127e27254..bde50ff20 100644 --- a/lib/build/recipe/multifactorauth/multiFactorAuthClaim.js +++ b/lib/build/recipe/multifactorauth/multiFactorAuthClaim.js @@ -1,18 +1,76 @@ "use strict"; +var __importDefault = + (this && this.__importDefault) || + function (mod) { + return mod && mod.__esModule ? mod : { default: mod }; + }; Object.defineProperty(exports, "__esModule", { value: true }); exports.MultiFactorAuthClaim = exports.MultiFactorAuthClaimClass = void 0; +const __1 = require("../.."); const claims_1 = require("../session/claims"); +const multitenancy_1 = __importDefault(require("../multitenancy")); +const recipe_1 = __importDefault(require("./recipe")); +const constants_1 = require("../multitenancy/constants"); +const utils_1 = require("./utils"); /** * We include "Class" in the class name, because it makes it easier to import the right thing (the instance) instead of this. * */ class MultiFactorAuthClaimClass extends claims_1.SessionClaim { constructor(key) { super(key !== null && key !== void 0 ? key : "st-mfa"); - this.fetchValue = (_userId, _recipeUserId, _tenantId, _userContext) => { - // TODO + this.fetchValue = async (userId, _recipeUserId, tenantId, currentPayload, userContext) => { + var _a, _b; + const user = await __1.getUser(userId, userContext); + if (user === undefined) { + throw new Error("Unknown User ID provided"); + } + const tenantInfo = await multitenancy_1.default.getTenant( + tenantId !== null && tenantId !== void 0 ? tenantId : constants_1.DEFAULT_TENANT_ID, + userContext + ); + if (tenantInfo === undefined) { + throw new Error("should never happen"); + } + const recipeInstance = recipe_1.default.getInstanceOrThrowError(); + const isAlreadySetup = await recipeInstance.recipeInterfaceImpl.getFactorsSetupForUser({ + user, + tenantId: tenantId !== null && tenantId !== void 0 ? tenantId : constants_1.DEFAULT_TENANT_ID, + userContext, + }); + // session is active and a new user is going to be created, so we need to check if the factor setup is allowed + const defaultRequiredFactorIdsForUser = await recipeInstance.recipeInterfaceImpl.getDefaultRequiredFactorsForUser( + { + user: user, + tenantId: tenantId !== null && tenantId !== void 0 ? tenantId : constants_1.DEFAULT_TENANT_ID, + userContext, + } + ); + const completedFactorsClaimValue = currentPayload && currentPayload[this.key]; + const completedFactors = + (_a = + completedFactorsClaimValue === null || completedFactorsClaimValue === void 0 + ? void 0 + : completedFactorsClaimValue.c) !== null && _a !== void 0 + ? _a + : {}; + const mfaRequirementsForAuth = await recipeInstance.recipeInterfaceImpl.getMFARequirementsForAuth({ + user, + accessTokenPayload: currentPayload !== undefined ? currentPayload : {}, + tenantId: tenantId !== null && tenantId !== void 0 ? tenantId : constants_1.DEFAULT_TENANT_ID, + factorsSetUpForUser: isAlreadySetup, + defaultRequiredFactorIdsForTenant: + (_b = + tenantInfo === null || tenantInfo === void 0 ? void 0 : tenantInfo.defaultRequiredFactorIds) !== + null && _b !== void 0 + ? _b + : [], + defaultRequiredFactorIdsForUser, + completedFactors: completedFactors, + userContext, + }); return { - c: {}, - n: [], + c: completedFactors, + n: exports.MultiFactorAuthClaim.buildNextArray(completedFactors, mfaRequirementsForAuth), }; }; this.addToPayload_internal = (payload, value) => { @@ -41,11 +99,130 @@ class MultiFactorAuthClaimClass extends claims_1.SessionClaim { return payload[this.key]; }; this.validators = { - passesMFARequirements: (_requirements) => ({}), // TODO + hasCompletedDefaultFactors: (id) => ({ + claim: this, + id: id !== null && id !== void 0 ? id : this.key, + shouldRefetch: (payload) => { + const value = this.getValueFromPayload(payload); + return value === undefined; + }, + validate: async (payload) => { + const claimVal = this.getValueFromPayload(payload); + if (claimVal === undefined) { + throw new Error("This should never happen, claim value not present in payload"); + } + const { n } = claimVal; + if (n.length === 0) { + return { + isValid: true, + }; + } + return { + isValid: false, + reason: { + message: "not all required factors have been completed", + nextFactorOptions: n, + }, + }; + }, + }), + hasCompletedFactors: (requirements, id) => ({ + claim: this, + id: id !== null && id !== void 0 ? id : this.key, + shouldRefetch: (payload) => { + const value = this.getValueFromPayload(payload); + return value === undefined; + }, + validate: async (payload) => { + if (requirements.length === 0) { + return { + isValid: true, // No requirements to satisfy + }; + } + const claimVal = this.getValueFromPayload(payload); + if (claimVal === undefined) { + throw new Error("This should never happen, claim value not present in payload"); + } + const { c } = claimVal; + for (const req of requirements) { + if (typeof req === "object" && "oneOf" in req) { + const res = req.oneOf + .map((r) => utils_1.checkFactorRequirement(r, c)) + .filter((v) => v.isValid === false); + if (res.length === req.oneOf.length) { + return { + isValid: false, + reason: { + message: "All factor checkers failed in the list", + oneOf: req.oneOf, + failures: res, + }, + }; + } + } else if (typeof req === "object" && "allOf" in req) { + const res = req.allOf + .map((r) => utils_1.checkFactorRequirement(r, c)) + .filter((v) => v.isValid === false); + if (res.length !== 0) { + return { + isValid: false, + reason: { + message: "Some factor checkers failed in the list", + allOf: req.allOf, + failures: res, + }, + }; + } + } else { + const res = utils_1.checkFactorRequirement(req, c); + if (res.isValid !== true) { + return { + isValid: false, + reason: { + message: "Factor validation failed: " + res.message, + factorId: res.id, + }, + }; + } + } + } + return { + isValid: true, + }; + }, + }), }; } - buildNextArray(_completedClaims, _requirements) { - // TODO + buildNextArray(completedClaims, requirements) { + for (const req of requirements) { + const nextFactors = new Set(); + if (typeof req === "string") { + if (completedClaims[req] === undefined) { + nextFactors.add(req); + } + } else if ("oneOf" in req) { + let satisfied = false; + for (const factorId of req.oneOf) { + if (completedClaims[factorId] !== undefined) { + satisfied = true; + } + } + if (!satisfied) { + for (const factorId of req.oneOf) { + nextFactors.add(factorId); + } + } + } else if ("allOf" in req) { + for (const factorId of req.allOf) { + if (completedClaims[factorId] === undefined) { + nextFactors.add(factorId); + } + } + } + if (nextFactors.size > 0) { + return Array.from(nextFactors); + } + } return []; } } diff --git a/lib/build/recipe/multifactorauth/recipe.d.ts b/lib/build/recipe/multifactorauth/recipe.d.ts index 332bced82..6214befb5 100644 --- a/lib/build/recipe/multifactorauth/recipe.d.ts +++ b/lib/build/recipe/multifactorauth/recipe.d.ts @@ -3,18 +3,31 @@ import { BaseRequest, BaseResponse } from "../../framework"; import NormalisedURLPath from "../../normalisedURLPath"; import RecipeModule from "../../recipeModule"; import STError from "../../error"; -import { APIHandled, HTTPMethod, NormalisedAppinfo, RecipeListFunction } from "../../types"; -import { ProviderInput } from "../thirdparty/types"; -import { APIInterface, RecipeInterface, TypeInput, TypeNormalisedInput } from "./types"; +import { APIHandled, HTTPMethod, NormalisedAppinfo, RecipeListFunction, UserContext } from "../../types"; +import { + APIInterface, + GetAllFactorsFromOtherRecipesFunc, + GetFactorsSetupForUserFromOtherRecipesFunc, + MFAFlowErrors, + RecipeInterface, + TypeInput, + TypeNormalisedInput, +} from "./types"; +import { User } from "../../user"; +import { SessionContainerInterface } from "../session/types"; +import RecipeUserId from "../../recipeUserId"; +import { Querier } from "../../querier"; +import { TenantConfig } from "../multitenancy/types"; export default class Recipe extends RecipeModule { private static instance; static RECIPE_ID: string; + getFactorsSetupForUserFromOtherRecipesFuncs: GetFactorsSetupForUserFromOtherRecipesFunc[]; + getAllFactorsFromOtherRecipesFunc: GetAllFactorsFromOtherRecipesFunc[]; config: TypeNormalisedInput; recipeInterfaceImpl: RecipeInterface; apiImpl: APIInterface; isInServerlessEnv: boolean; - staticThirdPartyProviders: ProviderInput[]; - getAllowedDomainsForTenantId?: (tenantId: string, userContext: any) => Promise; + querier: Querier; constructor(recipeId: string, appInfo: NormalisedAppinfo, isInServerlessEnv: boolean, config?: TypeInput); static getInstanceOrThrowError(): Recipe; static getInstance(): Recipe | undefined; @@ -28,9 +41,70 @@ export default class Recipe extends RecipeModule { res: BaseResponse, _: NormalisedURLPath, __: HTTPMethod, - userContext: any + userContext: UserContext ) => Promise; handleError: (err: STError, _: BaseRequest, __: BaseResponse) => Promise; getAllCORSHeaders: () => string[]; isErrorFromThisRecipe: (err: any) => err is STError; + addGetAllFactorsFromOtherRecipesFunc: (f: GetAllFactorsFromOtherRecipesFunc) => void; + getAllAvailableFactorIds: (tenantConfig: TenantConfig) => string[]; + getAllAvailableFirstFactorIds: (tenantConfig: TenantConfig) => string[]; + addGetFactorsSetupForUserFromOtherRecipes: (func: GetFactorsSetupForUserFromOtherRecipesFunc) => void; + validateForMultifactorAuthBeforeFactorCompletion: ({ + tenantId, + factorIdInProgress, + session, + userLoggingIn, + isAlreadySetup, + signUpInfo, + userContext, + }: { + req: BaseRequest; + res: BaseResponse; + tenantId: string; + factorIdInProgress: string; + session?: SessionContainerInterface | undefined; + userLoggingIn?: User | undefined; + isAlreadySetup?: boolean | undefined; + signUpInfo?: + | { + email: string; + isVerifiedFactor: boolean; + } + | undefined; + userContext: UserContext; + }) => Promise< + | { + status: "OK"; + } + | MFAFlowErrors + >; + createOrUpdateSessionForMultifactorAuthAfterFactorCompletion: ({ + req, + res, + tenantId, + factorIdInProgress, + justCompletedFactorUserInfo, + userContext, + }: { + req: BaseRequest; + res: BaseResponse; + tenantId: string; + factorIdInProgress: string; + isAlreadySetup?: boolean | undefined; + justCompletedFactorUserInfo?: + | { + user: User; + createdNewUser: boolean; + recipeUserId: RecipeUserId; + } + | undefined; + userContext: UserContext; + }) => Promise< + | MFAFlowErrors + | { + status: "OK"; + session: SessionContainerInterface; + } + >; } diff --git a/lib/build/recipe/multifactorauth/recipe.js b/lib/build/recipe/multifactorauth/recipe.js index 7d0b235bb..4cc78fce9 100644 --- a/lib/build/recipe/multifactorauth/recipe.js +++ b/lib/build/recipe/multifactorauth/recipe.js @@ -13,6 +13,17 @@ * License for the specific language governing permissions and limitations * under the License. */ +var __rest = + (this && this.__rest) || + function (s, e) { + var t = {}; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0) t[p] = s[p]; + if (s != null && typeof Object.getOwnPropertySymbols === "function") + for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) { + if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i])) t[p[i]] = s[p[i]]; + } + return t; + }; var __importDefault = (this && this.__importDefault) || function (mod) { @@ -21,7 +32,6 @@ var __importDefault = Object.defineProperty(exports, "__esModule", { value: true }); const supertokens_js_override_1 = __importDefault(require("supertokens-js-override")); const normalisedURLPath_1 = __importDefault(require("../../normalisedURLPath")); -const querier_1 = require("../../querier"); const recipeModule_1 = __importDefault(require("../../recipeModule")); const error_1 = __importDefault(require("../../error")); const recipeImplementation_1 = __importDefault(require("./recipeImplementation")); @@ -32,10 +42,18 @@ const utils_1 = require("./utils"); const mfaInfo_1 = __importDefault(require("./api/mfaInfo")); const recipe_1 = __importDefault(require("../session/recipe")); const postSuperTokensInitCallbacks_1 = require("../../postSuperTokensInitCallbacks"); +const recipeUserId_1 = __importDefault(require("../../recipeUserId")); +const multitenancy_1 = __importDefault(require("../multitenancy")); +const session_1 = __importDefault(require("../session")); +const recipe_2 = __importDefault(require("../accountlinking/recipe")); +const __1 = require("../.."); +const querier_1 = require("../../querier"); +const error_2 = __importDefault(require("../session/error")); class Recipe extends recipeModule_1.default { constructor(recipeId, appInfo, isInServerlessEnv, config) { super(recipeId, appInfo); - this.staticThirdPartyProviders = []; + this.getFactorsSetupForUserFromOtherRecipesFuncs = []; + this.getAllFactorsFromOtherRecipesFunc = []; // abstract instance functions below............... this.getAPIsHandled = () => { return [ @@ -49,13 +67,13 @@ class Recipe extends recipeModule_1.default { }; this.handleAPIRequest = async (id, _tenantId, req, res, _, __, userContext) => { let options = { + recipeInstance: this, recipeImplementation: this.recipeInterfaceImpl, config: this.config, recipeId: this.getRecipeId(), isInServerlessEnv: this.isInServerlessEnv, req, res, - staticThirdPartyProviders: this.staticThirdPartyProviders, }; if (id === constants_1.GET_MFA_INFO) { return await mfaInfo_1.default(this.apiImpl, options, userContext); @@ -71,18 +89,346 @@ class Recipe extends recipeModule_1.default { this.isErrorFromThisRecipe = (err) => { return error_1.default.isErrorFromSuperTokens(err) && err.fromRecipe === Recipe.RECIPE_ID; }; + this.addGetAllFactorsFromOtherRecipesFunc = (f) => { + this.getAllFactorsFromOtherRecipesFunc.push(f); + }; + this.getAllAvailableFactorIds = (tenantConfig) => { + let factorIds = []; + for (const func of this.getAllFactorsFromOtherRecipesFunc) { + const factorIdsRes = func(tenantConfig); + factorIds = factorIds.concat(factorIdsRes.factorIds); + } + return factorIds; + }; + this.getAllAvailableFirstFactorIds = (tenantConfig) => { + let factorIds = []; + for (const func of this.getAllFactorsFromOtherRecipesFunc) { + const factorIdsRes = func(tenantConfig); + factorIds = factorIds.concat(factorIdsRes.firstFactorIds); + } + return factorIds; + }; + this.addGetFactorsSetupForUserFromOtherRecipes = (func) => { + this.getFactorsSetupForUserFromOtherRecipesFuncs.push(func); + }; + this.validateForMultifactorAuthBeforeFactorCompletion = async ({ + tenantId, + factorIdInProgress, + session, + userLoggingIn, + isAlreadySetup, + signUpInfo, + userContext, + }) => { + var _a, _b, _c, _d; + const tenantInfo = await multitenancy_1.default.getTenant(tenantId, userContext); + const _e = tenantInfo, + { status: _ } = _e, + tenantConfig = __rest(_e, ["status"]); + const validFirstFactors = + (tenantInfo === null || tenantInfo === void 0 ? void 0 : tenantInfo.firstFactors) || + this.config.firstFactors || + this.getAllAvailableFirstFactorIds(tenantConfig); + if (session === undefined) { + // No session exists, so we need to check if it's a valid first factor before proceeding + if (!validFirstFactors.includes(factorIdInProgress)) { + return { + status: "DISALLOWED_FIRST_FACTOR_ERROR", + }; + } + return { + status: "OK", + }; + } + let sessionUser; + if (userLoggingIn) { + if (userLoggingIn.id !== session.getUserId()) { + // the user trying to login is not linked to the session user, based on session behaviour + // we just return OK and do nothing or replace replace the existing session with a new one + // we are doing this because we allow factor setup only when creating a new user + // this can happen when you got into login screen with an existing session and tried to log in with a different credentials + // or a case while doing secondary factor for phone otp but the user created a different account with the same phone number + return { + status: "OK", + }; + } + sessionUser = userLoggingIn; + } else { + sessionUser = await __1.getUser(session.getUserId(), userContext); + } + if (!sessionUser) { + // Session user doesn't exist, maybe the user was deleted + // Race condition, user got deleted in parallel, throw unauthorized + throw new error_2.default({ + type: error_2.default.UNAUTHORISED, + message: "Session user not found", + }); + } + if (isAlreadySetup) { + return { + status: "OK", + }; + } + // Check if the new user being created can be linked via MFA on the following conditions: + // 1. the new factor is a verified factor + // 2. the session user has a login method with same email and is verified + if (signUpInfo !== undefined) { + if (!signUpInfo.isVerifiedFactor) { + /* + We discussed another method but did not go ahead with it, details below: + + We can allow the second factor to be linked to first factor even if the emails are different + and not verified as long as there is no other user that exists (recipe or primary) that has the + same email as that of the second factor. For example, if first factor is google login with e1 + and second factor is email password with e2, we allow linking them as long as there is no other + user with email e2. + + We rejected this idea cause if auto account linking is switched off, then someone else can sign up + with google using e2. This is OK as it would not link (since account linking is switched off). + But, then if account linking is switched on, then the google sign in (and not sign up) with e2 + would actually cause it to be linked with the e1 account. + */ + let foundVerifiedEmail = false; + for (const lM of sessionUser === null || sessionUser === void 0 + ? void 0 + : sessionUser.loginMethods) { + if (lM.email === signUpInfo.email && lM.verified) { + foundVerifiedEmail = true; + break; + } + } + if (!foundVerifiedEmail) { + return { + status: "FACTOR_SETUP_NOT_ALLOWED_ERROR", + message: "Cannot setup factor as the email is not verified", + }; + } + } + // Check if there if the linking with session user going to fail and avoid user creation here + const users = await __1.listUsersByAccountInfo( + tenantId, + { email: signUpInfo.email }, + undefined, + userContext + ); + for (const user of users) { + if (user.isPrimaryUser && user.id !== sessionUser.id) { + return { + status: "ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR", + message: + "Cannot setup factor as the email is already associated with another primary user. Please contact support. (ERR_CODE_012)", + }; + } + } + } + // session is active and a new user is going to be created, so we need to check if the factor setup is allowed + const defaultRequiredFactorIdsForUser = await this.recipeInterfaceImpl.getDefaultRequiredFactorsForUser({ + user: sessionUser, + tenantId, + userContext, + }); + const factorsSetUpForUser = await this.recipeInterfaceImpl.getFactorsSetupForUser({ + user: sessionUser, + tenantId, + userContext, + }); + const completedFactorsClaimValue = await session.getClaimValue( + multiFactorAuthClaim_1.MultiFactorAuthClaim, + userContext + ); + const mfaRequirementsForAuth = await this.recipeInterfaceImpl.getMFARequirementsForAuth({ + user: sessionUser, + accessTokenPayload: session.getAccessTokenPayload(), + tenantId, + factorsSetUpForUser, + defaultRequiredFactorIdsForTenant: + (_a = + tenantInfo === null || tenantInfo === void 0 ? void 0 : tenantInfo.defaultRequiredFactorIds) !== + null && _a !== void 0 + ? _a + : [], + defaultRequiredFactorIdsForUser, + completedFactors: + (_b = + completedFactorsClaimValue === null || completedFactorsClaimValue === void 0 + ? void 0 + : completedFactorsClaimValue.c) !== null && _b !== void 0 + ? _b + : {}, + userContext, + }); + const canSetup = await this.recipeInterfaceImpl.isAllowedToSetupFactor({ + session, + factorId: factorIdInProgress, + completedFactors: + (_c = + completedFactorsClaimValue === null || completedFactorsClaimValue === void 0 + ? void 0 + : completedFactorsClaimValue.c) !== null && _c !== void 0 + ? _c + : {}, + defaultRequiredFactorIdsForTenant: + (_d = + tenantInfo === null || tenantInfo === void 0 ? void 0 : tenantInfo.defaultRequiredFactorIds) !== + null && _d !== void 0 + ? _d + : [], + defaultRequiredFactorIdsForUser, + factorsSetUpForUser, + mfaRequirementsForAuth, + userContext, + }); + if (!canSetup) { + return { + status: "FACTOR_SETUP_NOT_ALLOWED_ERROR", + }; + } + return { + status: "OK", + }; + }; + this.createOrUpdateSessionForMultifactorAuthAfterFactorCompletion = async ({ + req, + res, + tenantId, + factorIdInProgress, + justCompletedFactorUserInfo, + userContext, + }) => { + let session = await session_1.default.getSession(req, res, { + sessionRequired: false, + overrideGlobalClaimValidators: () => [], + }); + if ( + session === undefined // no session exists, so we can create a new one + ) { + if (justCompletedFactorUserInfo === undefined) { + throw new Error("should never come here"); // We wouldn't create new session from a recipe like TOTP + } + const newSession = await session_1.default.createNewSession( + req, + res, + tenantId, + justCompletedFactorUserInfo.recipeUserId, + {}, + {}, + userContext + ); + await this.recipeInterfaceImpl.markFactorAsCompleteInSession({ + session: newSession, + factorId: factorIdInProgress, + userContext, + }); + return { + status: "OK", + session: newSession, + }; + } + while (true) { + // loop to handle race conditions + const sessionUser = await __1.getUser(session.getUserId(), userContext); + // race condition, user deleted throw unauthorized + if (sessionUser === undefined) { + throw new error_2.default({ + type: error_2.default.UNAUTHORISED, + message: "Session user not found", + }); + } + if (justCompletedFactorUserInfo !== undefined) { + if (justCompletedFactorUserInfo.createdNewUser) { + // This is a newly created user, so it must be account linked with the session user + if (!sessionUser.isPrimaryUser) { + const createPrimaryRes = await recipe_2.default + .getInstance() + .recipeInterfaceImpl.createPrimaryUser({ + recipeUserId: new recipeUserId_1.default(sessionUser.id), + userContext, + }); + if ( + createPrimaryRes.status === "RECIPE_USER_ID_ALREADY_LINKED_WITH_PRIMARY_USER_ID_ERROR" + ) { + // Race condition + this.querier.invalidateCoreCallCache(userContext); + continue; + } else if ( + createPrimaryRes.status === + "ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR" + ) { + return { + status: "ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR", + message: + "Error setting up MFA for the user. Please contact support. (ERR_CODE_009)", + }; + } + } + const linkRes = await recipe_2.default.getInstance().recipeInterfaceImpl.linkAccounts({ + recipeUserId: justCompletedFactorUserInfo.recipeUserId, + primaryUserId: sessionUser.id, + userContext, + }); + if (linkRes.status === "RECIPE_USER_ID_ALREADY_LINKED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR") { + return { + status: "RECIPE_USER_ID_ALREADY_LINKED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR", + message: + "Error setting up MFA for the user because of the automatic account linking. Please contact support. (ERR_CODE_011)", + }; + } else if (linkRes.status === "INPUT_USER_IS_NOT_A_PRIMARY_USER") { + // Race condition + this.querier.invalidateCoreCallCache(userContext); + continue; + } else if ( + linkRes.status === "ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR" + ) { + return { + status: "ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR", + message: + "Cannot complete factor setup as the account info is already associated with another primary user. Please contact support. (ERR_CODE_010)", + }; + } + } else { + // Not a new user we should check if the user is linked to the session user + const loggedInUserLinkedToSessionUser = sessionUser.id === justCompletedFactorUserInfo.user.id; + if (!loggedInUserLinkedToSessionUser) { + // we may keep or replace the session as per the flag overwriteSessionDuringSignIn in session recipe + session = await session_1.default.createNewOrKeepExistingSession( + req, + res, + tenantId, + justCompletedFactorUserInfo.recipeUserId, + {}, + {}, + userContext + ); + return { + status: "OK", + session: session, + }; + } + } + } + break; + } + await this.recipeInterfaceImpl.markFactorAsCompleteInSession({ + session: session, + factorId: factorIdInProgress, + userContext, + }); + return { + status: "OK", + session: session, + }; + }; this.config = utils_1.validateAndNormaliseUserInput(config); this.isInServerlessEnv = isInServerlessEnv; { - let builder = new supertokens_js_override_1.default( - recipeImplementation_1.default(querier_1.Querier.getNewInstanceOrThrowError(recipeId)) - ); + let builder = new supertokens_js_override_1.default(recipeImplementation_1.default(this)); this.recipeInterfaceImpl = builder.override(this.config.override.functions).build(); } { let builder = new supertokens_js_override_1.default(implementation_1.default()); this.apiImpl = builder.override(this.config.override.apis).build(); } + this.querier = querier_1.Querier.getNewInstanceOrThrowError(recipeId); } static getInstanceOrThrowError() { if (Recipe.instance !== undefined) { @@ -103,7 +449,7 @@ class Recipe extends recipeModule_1.default { recipe_1.default .getInstanceOrThrowError() .addClaimValidatorFromOtherRecipe( - multiFactorAuthClaim_1.MultiFactorAuthClaim.validators.passesMFARequirements() + multiFactorAuthClaim_1.MultiFactorAuthClaim.validators.hasCompletedDefaultFactors() ); }); return Recipe.instance; diff --git a/lib/build/recipe/multifactorauth/recipeImplementation.d.ts b/lib/build/recipe/multifactorauth/recipeImplementation.d.ts index 6a2182ed3..fe6d93f74 100644 --- a/lib/build/recipe/multifactorauth/recipeImplementation.d.ts +++ b/lib/build/recipe/multifactorauth/recipeImplementation.d.ts @@ -1,4 +1,4 @@ // @ts-nocheck import { RecipeInterface } from "./"; -import { Querier } from "../../querier"; -export default function getRecipeInterface(querier: Querier): RecipeInterface; +import type MultiFactorAuthRecipe from "./recipe"; +export default function getRecipeInterface(recipeInstance: MultiFactorAuthRecipe): RecipeInterface; diff --git a/lib/build/recipe/multifactorauth/recipeImplementation.js b/lib/build/recipe/multifactorauth/recipeImplementation.js index c5add4e92..c30350d35 100644 --- a/lib/build/recipe/multifactorauth/recipeImplementation.js +++ b/lib/build/recipe/multifactorauth/recipeImplementation.js @@ -1,94 +1,170 @@ "use strict"; +var __rest = + (this && this.__rest) || + function (s, e) { + var t = {}; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0) t[p] = s[p]; + if (s != null && typeof Object.getOwnPropertySymbols === "function") + for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) { + if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i])) t[p[i]] = s[p[i]]; + } + return t; + }; var __importDefault = (this && this.__importDefault) || function (mod) { return mod && mod.__esModule ? mod : { default: mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); -const user_1 = require("../../user"); -const normalisedURLPath_1 = __importDefault(require("../../normalisedURLPath")); -function getRecipeInterface(querier) { +const recipe_1 = __importDefault(require("../usermetadata/recipe")); +const multiFactorAuthClaim_1 = require("./multiFactorAuthClaim"); +const multitenancy_1 = __importDefault(require("../multitenancy")); +const __1 = require("../.."); +const logger_1 = require("../../logger"); +function getRecipeInterface(recipeInstance) { return { - // markFactorAsCompleteInSession: async ({ session, factor, userContext }) => { - // const currentValue = await session.getClaimValue(MultiFactorAuthClaim); - // const completed = { - // ...currentValue?.c, - // [factor]: Math.floor(Date.now() / 1000), - // }; - // const setupUserFactors = await this.recipeInterfaceImpl.getFactorsSetupForUser({ - // userId: session.getUserId(), - // tenantId: session.getTenantId(), - // userContext, - // }); - // const requirements = await this.config.getMFARequirementsForAuth( - // session, - // setupUserFactors, - // completed, - // userContext - // ); - // const next = MultiFactorAuthClaim.buildNextArray(completed, requirements); - // await session.setClaimValue(MultiFactorAuthClaim, { - // c: completed, - // n: next, - // }); - // }, - createPrimaryUser: async function ({ recipeUserId, userContext }) { - let response = await querier.sendPostRequest( - new normalisedURLPath_1.default("/recipe/mfa/user/primary"), - { - recipeUserId: recipeUserId.getAsString(), - }, - userContext - ); - if (response.status === "OK") { - response.user = new user_1.User(response.user); + getFactorsSetupForUser: async function ({ tenantId, user, userContext }) { + const tenantInfo = await multitenancy_1.default.getTenant(tenantId, userContext); + if (tenantInfo === undefined) { + throw new Error("should never happen"); + } + let { status: _ } = tenantInfo, + tenantConfig = __rest(tenantInfo, ["status"]); + let factorIds = []; + for (const func of recipeInstance.getFactorsSetupForUserFromOtherRecipesFuncs) { + let result = await func(user, tenantConfig, userContext); + if (result !== undefined) { + factorIds = factorIds.concat(result); + } } - return response; + return factorIds; }, - linkAccounts: async function ({ recipeUserId, primaryUserId, userContext }) { - const accountsLinkingResult = await querier.sendPostRequest( - new normalisedURLPath_1.default("/recipe/accountlinking/user/link"), - { - recipeUserId: recipeUserId.getAsString(), - primaryUserId, - }, - userContext - ); - if ( - ["OK", "RECIPE_USER_ID_ALREADY_LINKED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR"].includes( - accountsLinkingResult.status - ) - ) { - accountsLinkingResult.user = new user_1.User(accountsLinkingResult.user); + getMFARequirementsForAuth: async function ({ + defaultRequiredFactorIdsForUser, + defaultRequiredFactorIdsForTenant, + completedFactors, + }) { + const loginTime = Math.min(...Object.values(completedFactors)); + const oldestFactor = Object.keys(completedFactors).find((k) => completedFactors[k] === loginTime); + const allFactors = new Set(); + for (const factor of defaultRequiredFactorIdsForUser) { + allFactors.add(factor); + } + for (const factor of defaultRequiredFactorIdsForTenant) { + allFactors.add(factor); + } + /* + We are removing only oldestFactor but not all factors considering the case below: + Assume a user has emailpassword as first factor, and otp-phone & totp as secondary factors. + + once the the user logs in with emailpassword, that's added to the completedFactors array. + Then user completes let's say otp-phone, that's added as well. + + Now when we try to build the next array, this function is called and we must return + { oneOf: ['otp-phone', 'totp'] }, so that the auth is assumed complete for the default opt-in 2FA behaviour. + + If we remove all completed factors and return { oneOf: ['totp' ]} at this point, this will force the + user to complete totp as well, which will result in a 3FA. which we don't intend to do by default. + */ + allFactors.delete(oldestFactor); // Removing the first factor if it exists + return [{ oneOf: [...allFactors] }]; + }, + isAllowedToSetupFactor: async function ({ factorId, session, factorsSetUpForUser, userContext }) { + const claimVal = await session.getClaimValue(multiFactorAuthClaim_1.MultiFactorAuthClaim, userContext); + if (!claimVal) { + throw new Error("should never happen"); } - // TODO check if the code below is required - // if (accountsLinkingResult.status === "OK") { - // let user: UserType = accountsLinkingResult.user; - // if (!accountsLinkingResult.accountsAlreadyLinked) { - // await recipeInstance.verifyEmailForRecipeUserIfLinkedAccountsAreVerified({ - // user: user, - // recipeUserId, - // userContext, - // }); - // const updatedUser = await this.getUser({ - // userId: primaryUserId, - // userContext, - // }); - // if (updatedUser === undefined) { - // throw Error("this error should never be thrown"); - // } - // user = updatedUser; - // let loginMethodInfo = user.loginMethods.find( - // (u) => u.recipeUserId.getAsString() === recipeUserId.getAsString() - // ); - // if (loginMethodInfo === undefined) { - // throw Error("this error should never be thrown"); - // } - // // await config.onAccountLinked(user, loginMethodInfo, userContext); - // } - // accountsLinkingResult.user = user; + // // This solution: checks for 2FA (we'd allow factor setup if the user has set up only 1 factor group or completed at least 2) + // const factorGroups = [ + // ["otp-phone", "link-phone"], + // ["otp-email", "link-email"], + // ["emailpassword"], + // ["thirdparty"], + // ]; + // const setUpGroups = Array.from( + // new Set(factorsSetUpForUser.map((id) => factorGroups.find((f) => f.includes(id)) || [id])) + // ); + // const completedGroups = setUpGroups.filter((group) => group.some((id) => claimVal.c[id] !== undefined)); + // // If the user completed every factor they could + // if (setUpGroups.length === completedGroups.length) { + // logDebugMessage( + // `isAllowedToSetupFactor ${factorId}: true because the user completed all factors they have set up and this is required` + // ); + // return true; // } - return accountsLinkingResult; + // return completedGroups.length >= 2; + if (claimVal.n.some((id) => factorsSetUpForUser.includes(id))) { + logger_1.logDebugMessage( + `isAllowedToSetupFactor ${factorId}: false because there are items already set up in the next array: ${claimVal.n.join( + ", " + )}` + ); + return false; + } + logger_1.logDebugMessage( + `isAllowedToSetupFactor ${factorId}: true because the next array is ${ + claimVal.n.length === 0 ? "empty" : "cannot be completed otherwise" + }` + ); + return true; + }, + markFactorAsCompleteInSession: async function ({ session, factorId, userContext }) { + var _a; + const currentValue = await session.getClaimValue(multiFactorAuthClaim_1.MultiFactorAuthClaim); + const completed = Object.assign( + Object.assign({}, currentValue === null || currentValue === void 0 ? void 0 : currentValue.c), + { [factorId]: Math.floor(Date.now() / 1000) } + ); + const tenantId = session.getTenantId(); + const user = await __1.getUser(session.getUserId(), userContext); + if (user === undefined) { + throw new Error("User not found!"); + } + const tenantInfo = await multitenancy_1.default.getTenant(tenantId, userContext); + const defaultRequiredFactorIdsForUser = await this.getDefaultRequiredFactorsForUser({ + user: user, + tenantId, + userContext, + }); + const factorsSetUpForUser = await this.getFactorsSetupForUser({ + user: user, + tenantId, + userContext, + }); + const mfaRequirementsForAuth = await this.getMFARequirementsForAuth({ + user, + accessTokenPayload: session.getAccessTokenPayload(), + tenantId, + factorsSetUpForUser, + defaultRequiredFactorIdsForTenant: + (_a = + tenantInfo === null || tenantInfo === void 0 ? void 0 : tenantInfo.defaultRequiredFactorIds) !== + null && _a !== void 0 + ? _a + : [], + defaultRequiredFactorIdsForUser, + completedFactors: completed, + userContext, + }); + const next = multiFactorAuthClaim_1.MultiFactorAuthClaim.buildNextArray(completed, mfaRequirementsForAuth); + await session.setClaimValue(multiFactorAuthClaim_1.MultiFactorAuthClaim, { + c: completed, + n: next, + }); + }, + getDefaultRequiredFactorsForUser: async function ({ user, userContext }) { + var _a, _b; + const userMetadataInstance = recipe_1.default.getInstanceOrThrowError(); + const metadata = await userMetadataInstance.recipeInterfaceImpl.getUserMetadata({ + userId: user.id, + userContext, + }); + return (_b = + (_a = metadata.metadata._supertokens) === null || _a === void 0 + ? void 0 + : _a.defaultRequiredFactorIdsForUser) !== null && _b !== void 0 + ? _b + : []; }, }; } diff --git a/lib/build/recipe/multifactorauth/types.d.ts b/lib/build/recipe/multifactorauth/types.d.ts index 8475160be..17b6f2098 100644 --- a/lib/build/recipe/multifactorauth/types.d.ts +++ b/lib/build/recipe/multifactorauth/types.d.ts @@ -1,10 +1,12 @@ // @ts-nocheck import { BaseRequest, BaseResponse } from "../../framework"; import OverrideableBuilder from "supertokens-js-override"; -import { GeneralErrorResponse, User } from "../../types"; +import { GeneralErrorResponse, JSONObject, UserContext } from "../../types"; +import { User } from "../../user"; import { SessionContainer } from "../session"; import { SessionContainerInterface } from "../session/types"; -import RecipeUserId from "../../recipeUserId"; +import Recipe from "./recipe"; +import { TenantConfig } from "../multitenancy/types"; export declare type MFARequirementList = ( | { oneOf: string[]; @@ -18,6 +20,14 @@ export declare type MFAClaimValue = { c: Record; n: string[]; }; +export declare type MFAFlowErrors = { + status: + | "DISALLOWED_FIRST_FACTOR_ERROR" + | "FACTOR_SETUP_NOT_ALLOWED_ERROR" + | "ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR" + | "RECIPE_USER_ID_ALREADY_LINKED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR"; + message?: string; +}; export declare type TypeInput = { firstFactors?: string[]; override?: { @@ -43,72 +53,37 @@ export declare type RecipeInterface = { session: SessionContainer; factorId: string; mfaRequirementsForAuth: MFARequirementList; - factorsSetUpByTheUser: string[]; + factorsSetUpForUser: string[]; defaultRequiredFactorIdsForUser: string[]; defaultRequiredFactorIdsForTenant: string[]; completedFactors: Record; - userContext: any; + userContext: UserContext; }) => Promise; getMFARequirementsForAuth: (input: { - session: SessionContainer; - factorsSetUpByTheUser: string[]; + user: User; + accessTokenPayload: JSONObject; + tenantId: string; + factorsSetUpForUser: string[]; defaultRequiredFactorIdsForUser: string[]; defaultRequiredFactorIdsForTenant: string[]; completedFactors: Record; - userContext: any; + userContext: UserContext; }) => Promise | MFARequirementList; markFactorAsCompleteInSession: (input: { session: SessionContainerInterface; factorId: string; - userContext?: any; + userContext: UserContext; }) => Promise; - getFactorsSetupForUser: (input: { user: User; tenantId: string; userContext: any }) => Promise; - createPrimaryUser: (input: { - recipeUserId: RecipeUserId; - userContext: any; - }) => Promise< - | { - status: "OK"; - user: User; - wasAlreadyAPrimaryUser: boolean; - } - | { - status: "RECIPE_USER_ID_ALREADY_LINKED_WITH_PRIMARY_USER_ID_ERROR"; - primaryUserId: string; - } - | { - status: "ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR"; - primaryUserId: string; - description: string; - } - >; - linkAccounts: (input: { - recipeUserId: RecipeUserId; - primaryUserId: string; - userContext: any; - }) => Promise< - | { - status: "OK"; - accountsAlreadyLinked: boolean; - user: User; - } - | { - status: "RECIPE_USER_ID_ALREADY_LINKED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR"; - primaryUserId: string; - user: User; - } - | { - status: "ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR"; - primaryUserId: string; - description: string; - } - | { - status: "INPUT_USER_IS_NOT_A_PRIMARY_USER"; - } - >; + getFactorsSetupForUser: (input: { tenantId: string; user: User; userContext: UserContext }) => Promise; + getDefaultRequiredFactorsForUser(input: { + user: User; + tenantId: string; + userContext: UserContext; + }): Promise; }; export declare type APIOptions = { recipeImplementation: RecipeInterface; + recipeInstance: Recipe; config: TypeNormalisedInput; recipeId: string; isInServerlessEnv: boolean; @@ -119,7 +94,7 @@ export declare type APIInterface = { mfaInfoGET: (input: { options: APIOptions; session: SessionContainerInterface; - userContext: any; + userContext: UserContext; }) => Promise< | { status: "OK"; @@ -127,7 +102,20 @@ export declare type APIInterface = { isAlreadySetup: string[]; isAllowedToSetup: string[]; }; + email?: string; + phoneNumber?: string; } | GeneralErrorResponse >; }; +export declare type GetFactorsSetupForUserFromOtherRecipesFunc = ( + user: User, + tenantConfig: TenantConfig, + userContext: UserContext +) => Promise; +export declare type GetAllFactorsFromOtherRecipesFunc = ( + tenantConfig: TenantConfig +) => { + factorIds: string[]; + firstFactorIds: string[]; +}; diff --git a/lib/build/recipe/multifactorauth/utils.d.ts b/lib/build/recipe/multifactorauth/utils.d.ts index 0cc991d9a..d0bbc1a55 100644 --- a/lib/build/recipe/multifactorauth/utils.d.ts +++ b/lib/build/recipe/multifactorauth/utils.d.ts @@ -1,3 +1,11 @@ // @ts-nocheck -import { TypeInput, TypeNormalisedInput } from "./types"; +import { TypeInput, TypeNormalisedInput, MFAClaimValue } from "./types"; export declare function validateAndNormaliseUserInput(config?: TypeInput): TypeNormalisedInput; +export declare function checkFactorRequirement( + req: string, + completedFactors: MFAClaimValue["c"] +): { + id: string; + isValid: boolean; + message: string; +}; diff --git a/lib/build/recipe/multifactorauth/utils.js b/lib/build/recipe/multifactorauth/utils.js index f7d24fc63..cd8f304e3 100644 --- a/lib/build/recipe/multifactorauth/utils.js +++ b/lib/build/recipe/multifactorauth/utils.js @@ -14,7 +14,7 @@ * under the License. */ Object.defineProperty(exports, "__esModule", { value: true }); -exports.validateAndNormaliseUserInput = void 0; +exports.checkFactorRequirement = exports.validateAndNormaliseUserInput = void 0; function validateAndNormaliseUserInput(config) { let override = Object.assign( { @@ -29,3 +29,11 @@ function validateAndNormaliseUserInput(config) { }; } exports.validateAndNormaliseUserInput = validateAndNormaliseUserInput; +function checkFactorRequirement(req, completedFactors) { + return { + id: req, + isValid: completedFactors[req] !== undefined, + message: "Not completed", + }; +} +exports.checkFactorRequirement = checkFactorRequirement; diff --git a/lib/build/recipe/multitenancy/allowedDomainsClaim.js b/lib/build/recipe/multitenancy/allowedDomainsClaim.js index adecb2009..d679919b5 100644 --- a/lib/build/recipe/multitenancy/allowedDomainsClaim.js +++ b/lib/build/recipe/multitenancy/allowedDomainsClaim.js @@ -15,7 +15,7 @@ class AllowedDomainsClaimClass extends claims_1.PrimitiveArrayClaim { constructor() { super({ key: "st-t-dmns", - async fetchValue(_userId, _recipeUserId, tenantId, userContext) { + async fetchValue(_userId, _recipeUserId, tenantId, _currentPayload, userContext) { const recipe = recipe_1.default.getInstanceOrThrowError(); if (recipe.getAllowedDomainsForTenantId === undefined) { return undefined; // User did not provide a function to get allowed domains, but is using a validator. So we don't allow any domains by default diff --git a/lib/build/recipe/multitenancy/api/loginMethods.d.ts b/lib/build/recipe/multitenancy/api/loginMethods.d.ts index f768660a4..c497a7557 100644 --- a/lib/build/recipe/multitenancy/api/loginMethods.d.ts +++ b/lib/build/recipe/multitenancy/api/loginMethods.d.ts @@ -1,8 +1,9 @@ // @ts-nocheck import { APIInterface, APIOptions } from "../"; +import { UserContext } from "../../../types"; export default function loginMethodsAPI( apiImplementation: APIInterface, tenantId: string, options: APIOptions, - userContext: any + userContext: UserContext ): Promise; diff --git a/lib/build/recipe/multitenancy/index.d.ts b/lib/build/recipe/multitenancy/index.d.ts index 0bd0e09d0..ae18789c5 100644 --- a/lib/build/recipe/multitenancy/index.d.ts +++ b/lib/build/recipe/multitenancy/index.d.ts @@ -1,6 +1,6 @@ // @ts-nocheck import Recipe from "./recipe"; -import { RecipeInterface, APIOptions, APIInterface } from "./types"; +import { RecipeInterface, APIOptions, APIInterface, TenantConfig } from "./types"; import { ProviderConfig } from "../thirdparty/types"; import { AllowedDomainsClaim } from "./allowedDomainsClaim"; import RecipeUserId from "../../recipeUserId"; @@ -12,70 +12,47 @@ export default class Wrapper { emailPasswordEnabled?: boolean; passwordlessEnabled?: boolean; thirdPartyEnabled?: boolean; + totpEnabled?: boolean; + firstFactors?: string[]; + defaultRequiredFactorIds?: string[]; coreConfig?: { [key: string]: any; }; }, - userContext?: any + userContext?: Record ): Promise<{ status: "OK"; createdNew: boolean; }>; static deleteTenant( tenantId: string, - userContext?: any + userContext?: Record ): Promise<{ status: "OK"; didExist: boolean; }>; static getTenant( tenantId: string, - userContext?: any + userContext?: Record ): Promise< - | { + | ({ status: "OK"; - emailPassword: { - enabled: boolean; - }; - passwordless: { - enabled: boolean; - }; - thirdParty: { - enabled: boolean; - providers: ProviderConfig[]; - }; - coreConfig: { - [key: string]: any; - }; - } + } & TenantConfig) | undefined >; static listAllTenants( - userContext?: any + userContext?: Record ): Promise<{ status: "OK"; - tenants: { + tenants: ({ tenantId: string; - emailPassword: { - enabled: boolean; - }; - passwordless: { - enabled: boolean; - }; - thirdParty: { - enabled: boolean; - providers: ProviderConfig[]; - }; - coreConfig: { - [key: string]: any; - }; - }[]; + } & TenantConfig)[]; }>; static createOrUpdateThirdPartyConfig( tenantId: string, config: ProviderConfig, skipValidation?: boolean, - userContext?: any + userContext?: Record ): Promise<{ status: "OK"; createdNew: boolean; @@ -83,7 +60,7 @@ export default class Wrapper { static deleteThirdPartyConfig( tenantId: string, thirdPartyId: string, - userContext?: any + userContext?: Record ): Promise<{ status: "OK"; didConfigExist: boolean; @@ -91,7 +68,7 @@ export default class Wrapper { static associateUserToTenant( tenantId: string, recipeUserId: RecipeUserId, - userContext?: any + userContext?: Record ): Promise< | { status: "OK"; @@ -112,7 +89,7 @@ export default class Wrapper { static disassociateUserFromTenant( tenantId: string, recipeUserId: RecipeUserId, - userContext?: any + userContext?: Record ): Promise<{ status: "OK"; wasAssociated: boolean; diff --git a/lib/build/recipe/multitenancy/index.js b/lib/build/recipe/multitenancy/index.js index c96023c92..bb6446939 100644 --- a/lib/build/recipe/multitenancy/index.js +++ b/lib/build/recipe/multitenancy/index.js @@ -28,33 +28,34 @@ Object.defineProperty(exports, "AllowedDomainsClaim", { return allowedDomainsClaim_1.AllowedDomainsClaim; }, }); +const utils_1 = require("../../utils"); class Wrapper { static async createOrUpdateTenant(tenantId, config, userContext) { const recipeInstance = recipe_1.default.getInstanceOrThrowError(); return recipeInstance.recipeInterfaceImpl.createOrUpdateTenant({ tenantId, config, - userContext: userContext === undefined ? {} : userContext, + userContext: utils_1.getUserContext(userContext), }); } static async deleteTenant(tenantId, userContext) { const recipeInstance = recipe_1.default.getInstanceOrThrowError(); return recipeInstance.recipeInterfaceImpl.deleteTenant({ tenantId, - userContext: userContext === undefined ? {} : userContext, + userContext: utils_1.getUserContext(userContext), }); } static async getTenant(tenantId, userContext) { const recipeInstance = recipe_1.default.getInstanceOrThrowError(); return recipeInstance.recipeInterfaceImpl.getTenant({ tenantId, - userContext: userContext === undefined ? {} : userContext, + userContext: utils_1.getUserContext(userContext), }); } static async listAllTenants(userContext) { const recipeInstance = recipe_1.default.getInstanceOrThrowError(); return recipeInstance.recipeInterfaceImpl.listAllTenants({ - userContext: userContext === undefined ? {} : userContext, + userContext: utils_1.getUserContext(userContext), }); } static async createOrUpdateThirdPartyConfig(tenantId, config, skipValidation, userContext) { @@ -63,7 +64,7 @@ class Wrapper { tenantId, config, skipValidation, - userContext: userContext === undefined ? {} : userContext, + userContext: utils_1.getUserContext(userContext), }); } static async deleteThirdPartyConfig(tenantId, thirdPartyId, userContext) { @@ -71,7 +72,7 @@ class Wrapper { return recipeInstance.recipeInterfaceImpl.deleteThirdPartyConfig({ tenantId, thirdPartyId, - userContext: userContext === undefined ? {} : userContext, + userContext: utils_1.getUserContext(userContext), }); } static async associateUserToTenant(tenantId, recipeUserId, userContext) { @@ -79,7 +80,7 @@ class Wrapper { return recipeInstance.recipeInterfaceImpl.associateUserToTenant({ tenantId, recipeUserId, - userContext: userContext === undefined ? {} : userContext, + userContext: utils_1.getUserContext(userContext), }); } static async disassociateUserFromTenant(tenantId, recipeUserId, userContext) { @@ -87,7 +88,7 @@ class Wrapper { return recipeInstance.recipeInterfaceImpl.disassociateUserFromTenant({ tenantId, recipeUserId, - userContext: userContext === undefined ? {} : userContext, + userContext: utils_1.getUserContext(userContext), }); } } diff --git a/lib/build/recipe/multitenancy/recipe.d.ts b/lib/build/recipe/multitenancy/recipe.d.ts index d3e683ce6..c726e140a 100644 --- a/lib/build/recipe/multitenancy/recipe.d.ts +++ b/lib/build/recipe/multitenancy/recipe.d.ts @@ -3,7 +3,7 @@ import { BaseRequest, BaseResponse } from "../../framework"; import NormalisedURLPath from "../../normalisedURLPath"; import RecipeModule from "../../recipeModule"; import STError from "../../error"; -import { APIHandled, HTTPMethod, NormalisedAppinfo, RecipeListFunction } from "../../types"; +import { APIHandled, HTTPMethod, NormalisedAppinfo, RecipeListFunction, UserContext } from "../../types"; import { ProviderInput } from "../thirdparty/types"; import { APIInterface, RecipeInterface, TypeInput, TypeNormalisedInput } from "./types"; export default class Recipe extends RecipeModule { @@ -14,7 +14,7 @@ export default class Recipe extends RecipeModule { apiImpl: APIInterface; isInServerlessEnv: boolean; staticThirdPartyProviders: ProviderInput[]; - getAllowedDomainsForTenantId?: (tenantId: string, userContext: any) => Promise; + getAllowedDomainsForTenantId?: (tenantId: string, userContext: UserContext) => Promise; constructor(recipeId: string, appInfo: NormalisedAppinfo, isInServerlessEnv: boolean, config?: TypeInput); static getInstanceOrThrowError(): Recipe; static getInstance(): Recipe | undefined; @@ -28,7 +28,7 @@ export default class Recipe extends RecipeModule { res: BaseResponse, _: NormalisedURLPath, __: HTTPMethod, - userContext: any + userContext: UserContext ) => Promise; handleError: (err: STError, _: BaseRequest, __: BaseResponse) => Promise; getAllCORSHeaders: () => string[]; diff --git a/lib/build/recipe/multitenancy/types.d.ts b/lib/build/recipe/multitenancy/types.d.ts index f9bbcb2ca..8473af4a9 100644 --- a/lib/build/recipe/multitenancy/types.d.ts +++ b/lib/build/recipe/multitenancy/types.d.ts @@ -2,10 +2,10 @@ import { BaseRequest, BaseResponse } from "../../framework"; import OverrideableBuilder from "supertokens-js-override"; import { ProviderConfig, ProviderInput } from "../thirdparty/types"; -import { GeneralErrorResponse } from "../../types"; +import { GeneralErrorResponse, UserContext } from "../../types"; import RecipeUserId from "../../recipeUserId"; export declare type TypeInput = { - getAllowedDomainsForTenantId?: (tenantId: string, userContext: any) => Promise; + getAllowedDomainsForTenantId?: (tenantId: string, userContext: UserContext) => Promise; override?: { functions?: ( originalImplementation: RecipeInterface, @@ -15,7 +15,7 @@ export declare type TypeInput = { }; }; export declare type TypeNormalisedInput = { - getAllowedDomainsForTenantId?: (tenantId: string, userContext: any) => Promise; + getAllowedDomainsForTenantId?: (tenantId: string, userContext: UserContext) => Promise; override: { functions: ( originalImplementation: RecipeInterface, @@ -24,78 +24,75 @@ export declare type TypeNormalisedInput = { apis: (originalImplementation: APIInterface, builder?: OverrideableBuilder) => APIInterface; }; }; +export declare type TenantConfig = { + emailPassword: { + enabled: boolean; + }; + passwordless: { + enabled: boolean; + }; + thirdParty: { + enabled: boolean; + providers: ProviderConfig[]; + }; + totp: { + enabled: boolean; + }; + firstFactors?: string[]; + defaultRequiredFactorIds?: string[]; + coreConfig: { + [key: string]: any; + }; +}; export declare type RecipeInterface = { - getTenantId: (input: { tenantIdFromFrontend: string; userContext: any }) => Promise; + getTenantId: (input: { tenantIdFromFrontend: string; userContext: UserContext }) => Promise; createOrUpdateTenant: (input: { tenantId: string; config?: { emailPasswordEnabled?: boolean; passwordlessEnabled?: boolean; thirdPartyEnabled?: boolean; + totpEnabled?: boolean; + firstFactors?: string[]; + defaultRequiredFactorIds?: string[]; coreConfig?: { [key: string]: any; }; }; - userContext: any; + userContext: UserContext; }) => Promise<{ status: "OK"; createdNew: boolean; }>; deleteTenant: (input: { tenantId: string; - userContext: any; + userContext: UserContext; }) => Promise<{ status: "OK"; didExist: boolean; }>; getTenant: (input: { tenantId: string; - userContext: any; + userContext: UserContext; }) => Promise< - | { + | ({ status: "OK"; - emailPassword: { - enabled: boolean; - }; - passwordless: { - enabled: boolean; - }; - thirdParty: { - enabled: boolean; - providers: ProviderConfig[]; - }; - coreConfig: { - [key: string]: any; - }; - } + } & TenantConfig) | undefined >; listAllTenants: (input: { - userContext: any; + userContext: UserContext; }) => Promise<{ status: "OK"; - tenants: { + tenants: (TenantConfig & { tenantId: string; - emailPassword: { - enabled: boolean; - }; - passwordless: { - enabled: boolean; - }; - thirdParty: { - enabled: boolean; - providers: ProviderConfig[]; - }; - coreConfig: { - [key: string]: any; - }; - }[]; + })[]; }>; createOrUpdateThirdPartyConfig: (input: { tenantId: string; config: ProviderConfig; skipValidation?: boolean; - userContext: any; + userContext: UserContext; }) => Promise<{ status: "OK"; createdNew: boolean; @@ -103,7 +100,7 @@ export declare type RecipeInterface = { deleteThirdPartyConfig: (input: { tenantId: string; thirdPartyId: string; - userContext: any; + userContext: UserContext; }) => Promise<{ status: "OK"; didConfigExist: boolean; @@ -111,7 +108,7 @@ export declare type RecipeInterface = { associateUserToTenant: (input: { tenantId: string; recipeUserId: RecipeUserId; - userContext: any; + userContext: UserContext; }) => Promise< | { status: "OK"; @@ -132,7 +129,7 @@ export declare type RecipeInterface = { disassociateUserFromTenant: (input: { tenantId: string; recipeUserId: RecipeUserId; - userContext: any; + userContext: UserContext; }) => Promise<{ status: "OK"; wasAssociated: boolean; @@ -152,7 +149,7 @@ export declare type APIInterface = { tenantId: string; clientType?: string; options: APIOptions; - userContext: any; + userContext: UserContext; }) => Promise< | { status: "OK"; diff --git a/lib/build/recipe/openid/api/getOpenIdDiscoveryConfiguration.d.ts b/lib/build/recipe/openid/api/getOpenIdDiscoveryConfiguration.d.ts index 4b481508d..45955e60f 100644 --- a/lib/build/recipe/openid/api/getOpenIdDiscoveryConfiguration.d.ts +++ b/lib/build/recipe/openid/api/getOpenIdDiscoveryConfiguration.d.ts @@ -1,7 +1,8 @@ // @ts-nocheck +import { UserContext } from "../../../types"; import { APIInterface, APIOptions } from "../types"; export default function getOpenIdDiscoveryConfiguration( apiImplementation: APIInterface, options: APIOptions, - userContext: any + userContext: UserContext ): Promise; diff --git a/lib/build/recipe/openid/api/getOpenIdDiscoveryConfiguration.js b/lib/build/recipe/openid/api/getOpenIdDiscoveryConfiguration.js index 000d7f489..b308bfffb 100644 --- a/lib/build/recipe/openid/api/getOpenIdDiscoveryConfiguration.js +++ b/lib/build/recipe/openid/api/getOpenIdDiscoveryConfiguration.js @@ -1,19 +1,5 @@ "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ const utils_1 = require("../../../utils"); async function getOpenIdDiscoveryConfiguration(apiImplementation, options, userContext) { if (apiImplementation.getOpenIdDiscoveryConfigurationGET === undefined) { diff --git a/lib/build/recipe/openid/index.d.ts b/lib/build/recipe/openid/index.d.ts index e15d4da40..e94fd0092 100644 --- a/lib/build/recipe/openid/index.d.ts +++ b/lib/build/recipe/openid/index.d.ts @@ -3,7 +3,7 @@ import OpenIdRecipe from "./recipe"; export default class OpenIdRecipeWrapper { static init: typeof OpenIdRecipe.init; static getOpenIdDiscoveryConfiguration( - userContext?: any + userContext?: Record ): Promise<{ status: "OK"; issuer: string; @@ -13,7 +13,7 @@ export default class OpenIdRecipeWrapper { payload?: any, validitySeconds?: number, useStaticSigningKey?: boolean, - userContext?: any + userContext?: Record ): Promise< | { status: "OK"; @@ -24,7 +24,7 @@ export default class OpenIdRecipeWrapper { } >; static getJWKS( - userContext?: any + userContext?: Record ): Promise<{ keys: import("../jwt").JsonWebKey[]; validityInSeconds?: number | undefined; diff --git a/lib/build/recipe/openid/index.js b/lib/build/recipe/openid/index.js index 8eb1cf8a8..7fe6c9681 100644 --- a/lib/build/recipe/openid/index.js +++ b/lib/build/recipe/openid/index.js @@ -6,11 +6,12 @@ var __importDefault = }; Object.defineProperty(exports, "__esModule", { value: true }); exports.getJWKS = exports.createJWT = exports.getOpenIdDiscoveryConfiguration = exports.init = void 0; +const utils_1 = require("../../utils"); const recipe_1 = __importDefault(require("./recipe")); class OpenIdRecipeWrapper { static getOpenIdDiscoveryConfiguration(userContext) { return recipe_1.default.getInstanceOrThrowError().recipeImplementation.getOpenIdDiscoveryConfiguration({ - userContext: userContext === undefined ? {} : userContext, + userContext: utils_1.getUserContext(userContext), }); } static createJWT(payload, validitySeconds, useStaticSigningKey, userContext) { @@ -18,12 +19,12 @@ class OpenIdRecipeWrapper { payload, validitySeconds, useStaticSigningKey, - userContext: userContext === undefined ? {} : userContext, + userContext: utils_1.getUserContext(userContext), }); } static getJWKS(userContext) { return recipe_1.default.getInstanceOrThrowError().jwtRecipe.recipeInterfaceImpl.getJWKS({ - userContext: userContext === undefined ? {} : userContext, + userContext: utils_1.getUserContext(userContext), }); } } diff --git a/lib/build/recipe/openid/recipe.d.ts b/lib/build/recipe/openid/recipe.d.ts index ca59ef755..8091cb365 100644 --- a/lib/build/recipe/openid/recipe.d.ts +++ b/lib/build/recipe/openid/recipe.d.ts @@ -3,7 +3,7 @@ import STError from "../../error"; import type { BaseRequest, BaseResponse } from "../../framework"; import normalisedURLPath from "../../normalisedURLPath"; import RecipeModule from "../../recipeModule"; -import { APIHandled, HTTPMethod, NormalisedAppinfo, RecipeListFunction } from "../../types"; +import { APIHandled, HTTPMethod, NormalisedAppinfo, RecipeListFunction, UserContext } from "../../types"; import { APIInterface, RecipeInterface, TypeInput, TypeNormalisedInput } from "./types"; import JWTRecipe from "../jwt/recipe"; export default class OpenIdRecipe extends RecipeModule { @@ -25,9 +25,14 @@ export default class OpenIdRecipe extends RecipeModule { response: BaseResponse, path: normalisedURLPath, method: HTTPMethod, - userContext: any + userContext: UserContext ) => Promise; - handleError: (error: STError, request: BaseRequest, response: BaseResponse) => Promise; + handleError: ( + error: STError, + request: BaseRequest, + response: BaseResponse, + userContext: UserContext + ) => Promise; getAllCORSHeaders: () => string[]; isErrorFromThisRecipe: (err: any) => err is STError; } diff --git a/lib/build/recipe/openid/recipe.js b/lib/build/recipe/openid/recipe.js index 03827ba3f..2144a2d2d 100644 --- a/lib/build/recipe/openid/recipe.js +++ b/lib/build/recipe/openid/recipe.js @@ -57,11 +57,11 @@ class OpenIdRecipe extends recipeModule_1.default { return this.jwtRecipe.handleAPIRequest(id, tenantId, req, response, path, method, userContext); } }; - this.handleError = async (error, request, response) => { + this.handleError = async (error, request, response, userContext) => { if (error.fromRecipe === OpenIdRecipe.RECIPE_ID) { throw error; } else { - return await this.jwtRecipe.handleError(error, request, response); + return await this.jwtRecipe.handleError(error, request, response, userContext); } }; this.getAllCORSHeaders = () => { diff --git a/lib/build/recipe/openid/types.d.ts b/lib/build/recipe/openid/types.d.ts index ccb4e6561..5b907a8d5 100644 --- a/lib/build/recipe/openid/types.d.ts +++ b/lib/build/recipe/openid/types.d.ts @@ -4,7 +4,7 @@ import type { BaseRequest, BaseResponse } from "../../framework"; import NormalisedURLDomain from "../../normalisedURLDomain"; import NormalisedURLPath from "../../normalisedURLPath"; import { RecipeInterface as JWTRecipeInterface, APIInterface as JWTAPIInterface, JsonWebKey } from "../jwt/types"; -import { GeneralErrorResponse } from "../../types"; +import { GeneralErrorResponse, UserContext } from "../../types"; export declare type TypeInput = { issuer?: string; jwtValiditySeconds?: number; @@ -60,7 +60,7 @@ export declare type APIInterface = { | undefined | ((input: { options: APIOptions; - userContext: any; + userContext: UserContext; }) => Promise< | { status: "OK"; @@ -72,7 +72,7 @@ export declare type APIInterface = { }; export declare type RecipeInterface = { getOpenIdDiscoveryConfiguration(input: { - userContext: any; + userContext: UserContext; }): Promise<{ status: "OK"; issuer: string; @@ -82,7 +82,7 @@ export declare type RecipeInterface = { payload?: any; validitySeconds?: number; useStaticSigningKey?: boolean; - userContext: any; + userContext: UserContext; }): Promise< | { status: "OK"; @@ -93,7 +93,7 @@ export declare type RecipeInterface = { } >; getJWKS(input: { - userContext: any; + userContext: UserContext; }): Promise<{ keys: JsonWebKey[]; }>; diff --git a/lib/build/recipe/passwordless/api/consumeCode.d.ts b/lib/build/recipe/passwordless/api/consumeCode.d.ts index 4b200001b..a07ff159f 100644 --- a/lib/build/recipe/passwordless/api/consumeCode.d.ts +++ b/lib/build/recipe/passwordless/api/consumeCode.d.ts @@ -1,8 +1,9 @@ // @ts-nocheck import { APIInterface, APIOptions } from ".."; +import { UserContext } from "../../../types"; export default function consumeCode( apiImplementation: APIInterface, tenantId: string, options: APIOptions, - userContext: any + userContext: UserContext ): Promise; diff --git a/lib/build/recipe/passwordless/api/createCode.d.ts b/lib/build/recipe/passwordless/api/createCode.d.ts index 93d5cda29..1d2619c75 100644 --- a/lib/build/recipe/passwordless/api/createCode.d.ts +++ b/lib/build/recipe/passwordless/api/createCode.d.ts @@ -1,8 +1,9 @@ // @ts-nocheck import { APIInterface, APIOptions } from ".."; +import { UserContext } from "../../../types"; export default function createCode( apiImplementation: APIInterface, tenantId: string, options: APIOptions, - userContext: any + userContext: UserContext ): Promise; diff --git a/lib/build/recipe/passwordless/api/emailExists.d.ts b/lib/build/recipe/passwordless/api/emailExists.d.ts index 42894445f..2f55b6d3b 100644 --- a/lib/build/recipe/passwordless/api/emailExists.d.ts +++ b/lib/build/recipe/passwordless/api/emailExists.d.ts @@ -1,8 +1,9 @@ // @ts-nocheck import { APIInterface, APIOptions } from "../"; +import { UserContext } from "../../../types"; export default function emailExists( apiImplementation: APIInterface, tenantId: string, options: APIOptions, - userContext: any + userContext: UserContext ): Promise; diff --git a/lib/build/recipe/passwordless/api/implementation.js b/lib/build/recipe/passwordless/api/implementation.js index 9ac97df00..9a975fd8e 100644 --- a/lib/build/recipe/passwordless/api/implementation.js +++ b/lib/build/recipe/passwordless/api/implementation.js @@ -7,8 +7,11 @@ var __importDefault = Object.defineProperty(exports, "__esModule", { value: true }); const logger_1 = require("../../../logger"); const recipe_1 = __importDefault(require("../../accountlinking/recipe")); +const recipe_2 = __importDefault(require("../../multifactorauth/recipe")); const session_1 = __importDefault(require("../../session")); const __1 = require("../../.."); +const utils_1 = require("../utils"); +const error_1 = __importDefault(require("../../session/error")); function getAPIImplementation() { return { consumeCodePOST: async function (input) { @@ -63,6 +66,53 @@ function getAPIImplementation() { "You have found a bug. Please report it on https://github.com/supertokens/supertokens-node/issues" ); } + const userLoggingIn = existingUsers[0]; + const mfaInstance = recipe_2.default.getInstance(); + let session = await session_1.default.getSession(input.options.req, input.options.res, { + sessionRequired: false, + overrideGlobalClaimValidators: () => [], + }); + let sessionUser; + if (session !== undefined) { + if (userLoggingIn && userLoggingIn.id === session.getUserId()) { + sessionUser = userLoggingIn; // optimization + } else { + const user = await __1.getUser(session.getUserId(), input.userContext); + if (user === undefined) { + throw new error_1.default({ + type: error_1.default.UNAUTHORISED, + message: "Session user not found", + }); + } + sessionUser = user; + } + } + const factorId = `${"userInputCode" in input ? "otp" : "link"}-${deviceInfo.email ? "email" : "phone"}`; + let isAlreadySetup = undefined; + if (mfaInstance) { + isAlreadySetup = !sessionUser + ? false + : utils_1.isFactorSetupForUser(sessionUser, factorId) && + (deviceInfo.email + ? sessionUser.emails.includes(deviceInfo.email) + : sessionUser.phoneNumbers.includes(deviceInfo.phoneNumber)); + // We want to consider a factor as already setup only if email/phoneNumber of the userLoggingIn matches with the sessionUser emails/phoneNumbers + // because if it's a different email/phone number, it means we might be setting up that factor + const validateMfaRes = await mfaInstance.validateForMultifactorAuthBeforeFactorCompletion({ + req: input.options.req, + res: input.options.res, + tenantId: input.tenantId, + factorIdInProgress: factorId, + session, + userLoggingIn, + isAlreadySetup, + signUpInfo: deviceInfo.email ? { email: deviceInfo.email, isVerifiedFactor: true } : undefined, + userContext: input.userContext, + }); + if (validateMfaRes.status !== "OK") { + return validateMfaRes; + } + } let response = await input.options.recipeImplementation.consumeCode( "deviceId" in input ? { @@ -70,12 +120,16 @@ function getAPIImplementation() { deviceId: input.deviceId, userInputCode: input.userInputCode, tenantId: input.tenantId, + // we do not want to attempt accountlinking when there is an active session and MFA is turned on + shouldAttemptAccountLinkingIfAllowed: session === undefined || mfaInstance === undefined, userContext: input.userContext, } : { preAuthSessionId: input.preAuthSessionId, linkCode: input.linkCode, tenantId: input.tenantId, + // we do not want to attempt accountlinking when there is an active session and MFA is turned on + shouldAttemptAccountLinkingIfAllowed: session === undefined || mfaInstance === undefined, userContext: input.userContext, } ); @@ -111,25 +165,54 @@ function getAPIImplementation() { } // we do account linking only during sign in here cause during sign up, // the recipe function above does account linking for us. - response.user = await recipe_1.default.getInstance().createPrimaryUserIdOrLinkAccounts({ - tenantId: input.tenantId, + // we do not want to attempt accountlinking when there is an active session and MFA is turned on + if (session === undefined || mfaInstance === undefined) { + response.user = await recipe_1.default.getInstance().createPrimaryUserIdOrLinkAccounts({ + tenantId: input.tenantId, + user: response.user, + userContext: input.userContext, + }); + } + } + if (mfaInstance === undefined) { + // No MFA stuff here, so we just create and return the session + let session = await session_1.default.createNewOrKeepExistingSession( + input.options.req, + input.options.res, + input.tenantId, + response.recipeUserId, + {}, + {}, + input.userContext + ); + return { + status: "OK", + createdNewRecipeUser: response.createdNewRecipeUser, user: response.user, - userContext: input.userContext, - }); + session, + }; } - const session = await session_1.default.createNewSession( - input.options.req, - input.options.res, - input.tenantId, - loginMethod.recipeUserId, - {}, - {}, - input.userContext - ); + const sessionRes = await mfaInstance.createOrUpdateSessionForMultifactorAuthAfterFactorCompletion({ + req: input.options.req, + res: input.options.res, + tenantId: input.tenantId, + factorIdInProgress: factorId, + justCompletedFactorUserInfo: { + user: response.user, + createdNewUser: response.createdNewRecipeUser, + recipeUserId: response.recipeUserId, + }, + isAlreadySetup, + userContext: input.userContext, + }); + if (sessionRes.status !== "OK") { + return sessionRes; + } + session = sessionRes.session; return { status: "OK", createdNewRecipeUser: response.createdNewRecipeUser, - user: response.user, + user: await __1.getUser(response.user.id, input.userContext), session, }; }, diff --git a/lib/build/recipe/passwordless/api/phoneNumberExists.d.ts b/lib/build/recipe/passwordless/api/phoneNumberExists.d.ts index 340107a3e..9d545f9dc 100644 --- a/lib/build/recipe/passwordless/api/phoneNumberExists.d.ts +++ b/lib/build/recipe/passwordless/api/phoneNumberExists.d.ts @@ -1,8 +1,9 @@ // @ts-nocheck import { APIInterface, APIOptions } from ".."; +import { UserContext } from "../../../types"; export default function phoneNumberExists( apiImplementation: APIInterface, tenantId: string, options: APIOptions, - userContext: any + userContext: UserContext ): Promise; diff --git a/lib/build/recipe/passwordless/api/resendCode.d.ts b/lib/build/recipe/passwordless/api/resendCode.d.ts index 0aa546fc4..d6fd98191 100644 --- a/lib/build/recipe/passwordless/api/resendCode.d.ts +++ b/lib/build/recipe/passwordless/api/resendCode.d.ts @@ -1,8 +1,9 @@ // @ts-nocheck import { APIInterface, APIOptions } from ".."; +import { UserContext } from "../../../types"; export default function resendCode( apiImplementation: APIInterface, tenantId: string, options: APIOptions, - userContext: any + userContext: UserContext ): Promise; diff --git a/lib/build/recipe/passwordless/emaildelivery/services/backwardCompatibility/index.d.ts b/lib/build/recipe/passwordless/emaildelivery/services/backwardCompatibility/index.d.ts index 533582631..3c47b9d5a 100644 --- a/lib/build/recipe/passwordless/emaildelivery/services/backwardCompatibility/index.d.ts +++ b/lib/build/recipe/passwordless/emaildelivery/services/backwardCompatibility/index.d.ts @@ -1,14 +1,14 @@ // @ts-nocheck import { TypePasswordlessEmailDeliveryInput } from "../../../types"; import { EmailDeliveryInterface } from "../../../../../ingredients/emaildelivery/types"; -import { NormalisedAppinfo } from "../../../../../types"; +import { NormalisedAppinfo, UserContext } from "../../../../../types"; export default class BackwardCompatibilityService implements EmailDeliveryInterface { private appInfo; constructor(appInfo: NormalisedAppinfo); sendEmail: ( input: TypePasswordlessEmailDeliveryInput & { - userContext: any; + userContext: UserContext; } ) => Promise; } diff --git a/lib/build/recipe/passwordless/emaildelivery/services/smtp/index.d.ts b/lib/build/recipe/passwordless/emaildelivery/services/smtp/index.d.ts index 3cda03b71..188d41809 100644 --- a/lib/build/recipe/passwordless/emaildelivery/services/smtp/index.d.ts +++ b/lib/build/recipe/passwordless/emaildelivery/services/smtp/index.d.ts @@ -2,12 +2,13 @@ import { ServiceInterface, TypeInput } from "../../../../../ingredients/emaildelivery/services/smtp"; import { EmailDeliveryInterface } from "../../../../../ingredients/emaildelivery/types"; import { TypePasswordlessEmailDeliveryInput } from "../../../types"; +import { UserContext } from "../../../../../types"; export default class SMTPService implements EmailDeliveryInterface { serviceImpl: ServiceInterface; constructor(config: TypeInput); sendEmail: ( input: TypePasswordlessEmailDeliveryInput & { - userContext: any; + userContext: UserContext; } ) => Promise; } diff --git a/lib/build/recipe/passwordless/index.d.ts b/lib/build/recipe/passwordless/index.d.ts index 0fc90c85a..8bc2a319a 100644 --- a/lib/build/recipe/passwordless/index.d.ts +++ b/lib/build/recipe/passwordless/index.d.ts @@ -23,7 +23,7 @@ export default class Wrapper { ) & { tenantId: string; userInputCode?: string; - userContext?: any; + userContext?: Record; } ): Promise<{ status: "OK"; @@ -39,7 +39,7 @@ export default class Wrapper { deviceId: string; userInputCode?: string; tenantId: string; - userContext?: any; + userContext?: Record; }): Promise< | { status: "OK"; @@ -62,13 +62,15 @@ export default class Wrapper { userInputCode: string; deviceId: string; tenantId: string; - userContext?: any; + shouldAttemptAccountLinkingIfAllowed?: boolean; + userContext?: Record; } | { preAuthSessionId: string; linkCode: string; tenantId: string; - userContext?: any; + shouldAttemptAccountLinkingIfAllowed?: boolean; + userContext?: Record; } ): Promise< | { @@ -76,7 +78,6 @@ export default class Wrapper { createdNewRecipeUser: boolean; user: import("../../types").User; recipeUserId: RecipeUserId; - isValidFirstFactorForTenant: boolean | undefined; } | { status: "INCORRECT_USER_INPUT_CODE_ERROR" | "EXPIRED_USER_INPUT_CODE_ERROR"; @@ -91,7 +92,7 @@ export default class Wrapper { recipeUserId: RecipeUserId; email?: string | null; phoneNumber?: string | null; - userContext?: any; + userContext?: Record; }): Promise< | { status: @@ -110,12 +111,12 @@ export default class Wrapper { | { email: string; tenantId: string; - userContext?: any; + userContext?: Record; } | { phoneNumber: string; tenantId: string; - userContext?: any; + userContext?: Record; } ): Promise<{ status: "OK"; @@ -123,41 +124,41 @@ export default class Wrapper { static revokeCode(input: { codeId: string; tenantId: string; - userContext?: any; + userContext?: Record; }): Promise<{ status: "OK"; }>; static listCodesByEmail(input: { email: string; tenantId: string; - userContext?: any; + userContext?: Record; }): Promise; static listCodesByPhoneNumber(input: { phoneNumber: string; tenantId: string; - userContext?: any; + userContext?: Record; }): Promise; static listCodesByDeviceId(input: { deviceId: string; tenantId: string; - userContext?: any; + userContext?: Record; }): Promise; static listCodesByPreAuthSessionId(input: { preAuthSessionId: string; tenantId: string; - userContext?: any; + userContext?: Record; }): Promise; static createMagicLink( input: | { email: string; tenantId: string; - userContext?: any; + userContext?: Record; } | { phoneNumber: string; tenantId: string; - userContext?: any; + userContext?: Record; } ): Promise; static signInUp( @@ -165,28 +166,29 @@ export default class Wrapper { | { email: string; tenantId: string; - userContext?: any; + shouldAttemptAccountLinkingIfAllowed?: boolean; + userContext?: Record; } | { phoneNumber: string; tenantId: string; - userContext?: any; + shouldAttemptAccountLinkingIfAllowed?: boolean; + userContext?: Record; } ): Promise<{ status: string; createdNewRecipeUser: boolean; recipeUserId: RecipeUserId; user: import("../../types").User; - isValidFirstFactorForTenant: boolean | undefined; }>; static sendEmail( input: TypePasswordlessEmailDeliveryInput & { - userContext?: any; + userContext?: Record; } ): Promise; static sendSms( input: TypePasswordlessSmsDeliveryInput & { - userContext?: any; + userContext?: Record; } ): Promise; } diff --git a/lib/build/recipe/passwordless/index.js b/lib/build/recipe/passwordless/index.js index 7881e06dc..e81fbf353 100644 --- a/lib/build/recipe/passwordless/index.js +++ b/lib/build/recipe/passwordless/index.js @@ -23,116 +23,107 @@ exports.sendSms = exports.sendEmail = exports.signInUp = exports.createMagicLink const recipe_1 = __importDefault(require("./recipe")); const error_1 = __importDefault(require("./error")); const __1 = require("../.."); +const utils_1 = require("../../utils"); class Wrapper { static createCode(input) { - var _a; - return recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.createCode( - Object.assign(Object.assign({}, input), { - userContext: (_a = input.userContext) !== null && _a !== void 0 ? _a : {}, - }) - ); + return recipe_1.default + .getInstanceOrThrowError() + .recipeInterfaceImpl.createCode( + Object.assign(Object.assign({}, input), { userContext: utils_1.getUserContext(input.userContext) }) + ); } static createNewCodeForDevice(input) { - var _a; - return recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.createNewCodeForDevice( - Object.assign(Object.assign({}, input), { - userContext: (_a = input.userContext) !== null && _a !== void 0 ? _a : {}, - }) - ); + return recipe_1.default + .getInstanceOrThrowError() + .recipeInterfaceImpl.createNewCodeForDevice( + Object.assign(Object.assign({}, input), { userContext: utils_1.getUserContext(input.userContext) }) + ); } static consumeCode(input) { - var _a; - return recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.consumeCode( - Object.assign(Object.assign({}, input), { - userContext: (_a = input.userContext) !== null && _a !== void 0 ? _a : {}, - }) - ); + return recipe_1.default + .getInstanceOrThrowError() + .recipeInterfaceImpl.consumeCode( + Object.assign(Object.assign({}, input), { userContext: utils_1.getUserContext(input.userContext) }) + ); } static updateUser(input) { return recipe_1.default .getInstanceOrThrowError() - .recipeInterfaceImpl.updateUser(Object.assign({ userContext: {} }, input)); + .recipeInterfaceImpl.updateUser( + Object.assign(Object.assign({}, input), { userContext: utils_1.getUserContext(input.userContext) }) + ); } static revokeAllCodes(input) { - var _a; - return recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.revokeAllCodes( - Object.assign(Object.assign({}, input), { - userContext: (_a = input.userContext) !== null && _a !== void 0 ? _a : {}, - }) - ); + return recipe_1.default + .getInstanceOrThrowError() + .recipeInterfaceImpl.revokeAllCodes( + Object.assign(Object.assign({}, input), { userContext: utils_1.getUserContext(input.userContext) }) + ); } static revokeCode(input) { - var _a; - return recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.revokeCode( - Object.assign(Object.assign({}, input), { - userContext: (_a = input.userContext) !== null && _a !== void 0 ? _a : {}, - }) - ); + return recipe_1.default + .getInstanceOrThrowError() + .recipeInterfaceImpl.revokeCode( + Object.assign(Object.assign({}, input), { userContext: utils_1.getUserContext(input.userContext) }) + ); } static listCodesByEmail(input) { - var _a; - return recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.listCodesByEmail( - Object.assign(Object.assign({}, input), { - userContext: (_a = input.userContext) !== null && _a !== void 0 ? _a : {}, - }) - ); + return recipe_1.default + .getInstanceOrThrowError() + .recipeInterfaceImpl.listCodesByEmail( + Object.assign(Object.assign({}, input), { userContext: utils_1.getUserContext(input.userContext) }) + ); } static listCodesByPhoneNumber(input) { - var _a; - return recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.listCodesByPhoneNumber( - Object.assign(Object.assign({}, input), { - userContext: (_a = input.userContext) !== null && _a !== void 0 ? _a : {}, - }) - ); + return recipe_1.default + .getInstanceOrThrowError() + .recipeInterfaceImpl.listCodesByPhoneNumber( + Object.assign(Object.assign({}, input), { userContext: utils_1.getUserContext(input.userContext) }) + ); } static listCodesByDeviceId(input) { - var _a; - return recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.listCodesByDeviceId( - Object.assign(Object.assign({}, input), { - userContext: (_a = input.userContext) !== null && _a !== void 0 ? _a : {}, - }) - ); + return recipe_1.default + .getInstanceOrThrowError() + .recipeInterfaceImpl.listCodesByDeviceId( + Object.assign(Object.assign({}, input), { userContext: utils_1.getUserContext(input.userContext) }) + ); } static listCodesByPreAuthSessionId(input) { - var _a; - return recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.listCodesByPreAuthSessionId( - Object.assign(Object.assign({}, input), { - userContext: (_a = input.userContext) !== null && _a !== void 0 ? _a : {}, - }) - ); + return recipe_1.default + .getInstanceOrThrowError() + .recipeInterfaceImpl.listCodesByPreAuthSessionId( + Object.assign(Object.assign({}, input), { userContext: utils_1.getUserContext(input.userContext) }) + ); } static createMagicLink(input) { - var _a; + const ctx = utils_1.getUserContext(input.userContext); return recipe_1.default.getInstanceOrThrowError().createMagicLink( Object.assign(Object.assign({}, input), { - request: __1.getRequestFromUserContext(input.userContext), - userContext: (_a = input.userContext) !== null && _a !== void 0 ? _a : {}, + request: __1.getRequestFromUserContext(ctx), + userContext: ctx, }) ); } static signInUp(input) { - var _a; - return recipe_1.default.getInstanceOrThrowError().signInUp( - Object.assign(Object.assign({}, input), { - userContext: (_a = input.userContext) !== null && _a !== void 0 ? _a : {}, - }) - ); + return recipe_1.default + .getInstanceOrThrowError() + .signInUp( + Object.assign(Object.assign({}, input), { userContext: utils_1.getUserContext(input.userContext) }) + ); } static async sendEmail(input) { - var _a; - return await recipe_1.default.getInstanceOrThrowError().emailDelivery.ingredientInterfaceImpl.sendEmail( - Object.assign(Object.assign({}, input), { - userContext: (_a = input.userContext) !== null && _a !== void 0 ? _a : {}, - }) - ); + return await recipe_1.default + .getInstanceOrThrowError() + .emailDelivery.ingredientInterfaceImpl.sendEmail( + Object.assign(Object.assign({}, input), { userContext: utils_1.getUserContext(input.userContext) }) + ); } static async sendSms(input) { - var _a; - return await recipe_1.default.getInstanceOrThrowError().smsDelivery.ingredientInterfaceImpl.sendSms( - Object.assign(Object.assign({}, input), { - userContext: (_a = input.userContext) !== null && _a !== void 0 ? _a : {}, - }) - ); + return await recipe_1.default + .getInstanceOrThrowError() + .smsDelivery.ingredientInterfaceImpl.sendSms( + Object.assign(Object.assign({}, input), { userContext: utils_1.getUserContext(input.userContext) }) + ); } } exports.default = Wrapper; diff --git a/lib/build/recipe/passwordless/recipe.d.ts b/lib/build/recipe/passwordless/recipe.d.ts index f3a780af6..336d6c1e0 100644 --- a/lib/build/recipe/passwordless/recipe.d.ts +++ b/lib/build/recipe/passwordless/recipe.d.ts @@ -1,7 +1,7 @@ // @ts-nocheck import RecipeModule from "../../recipeModule"; import { TypeInput, TypeNormalisedInput, RecipeInterface, APIInterface } from "./types"; -import { NormalisedAppinfo, APIHandled, RecipeListFunction, HTTPMethod } from "../../types"; +import { NormalisedAppinfo, APIHandled, RecipeListFunction, HTTPMethod, UserContext } from "../../types"; import STError from "./error"; import NormalisedURLPath from "../../normalisedURLPath"; import type { BaseRequest, BaseResponse } from "../../framework"; @@ -38,7 +38,7 @@ export default class Recipe extends RecipeModule { res: BaseResponse, _: NormalisedURLPath, __: HTTPMethod, - userContext: any + userContext: UserContext ) => Promise; handleError: (err: STError, _: BaseRequest, __: BaseResponse) => Promise; getAllCORSHeaders: () => string[]; @@ -49,13 +49,13 @@ export default class Recipe extends RecipeModule { email: string; tenantId: string; request: BaseRequest | undefined; - userContext?: any; + userContext: UserContext; } | { phoneNumber: string; tenantId: string; request: BaseRequest | undefined; - userContext?: any; + userContext: UserContext; } ) => Promise; signInUp: ( @@ -63,18 +63,19 @@ export default class Recipe extends RecipeModule { | { email: string; tenantId: string; - userContext?: any; + shouldAttemptAccountLinkingIfAllowed?: boolean; + userContext: UserContext; } | { phoneNumber: string; tenantId: string; - userContext?: any; + shouldAttemptAccountLinkingIfAllowed?: boolean; + userContext: UserContext; } ) => Promise<{ status: string; createdNewRecipeUser: boolean; recipeUserId: import("../..").RecipeUserId; user: import("../../types").User; - isValidFirstFactorForTenant: boolean | undefined; }>; } diff --git a/lib/build/recipe/passwordless/recipe.js b/lib/build/recipe/passwordless/recipe.js index 49b9c6139..2400fc859 100644 --- a/lib/build/recipe/passwordless/recipe.js +++ b/lib/build/recipe/passwordless/recipe.js @@ -35,6 +35,9 @@ const resendCode_1 = __importDefault(require("./api/resendCode")); const constants_1 = require("./constants"); const emaildelivery_1 = __importDefault(require("../../ingredients/emaildelivery")); const smsdelivery_1 = __importDefault(require("../../ingredients/smsdelivery")); +const postSuperTokensInitCallbacks_1 = require("../../postSuperTokensInitCallbacks"); +const recipe_1 = __importDefault(require("../multifactorauth/recipe")); +const utils_2 = require("./utils"); class Recipe extends recipeModule_1.default { constructor(recipeId, appInfo, isInServerlessEnv, config, ingredients) { super(recipeId, appInfo); @@ -167,6 +170,7 @@ class Recipe extends recipeModule_1.default { preAuthSessionId: codeInfo.preAuthSessionId, linkCode: codeInfo.linkCode, tenantId: input.tenantId, + shouldAttemptAccountLinkingIfAllowed: input.shouldAttemptAccountLinkingIfAllowed, userContext: input.userContext, } : { @@ -174,6 +178,7 @@ class Recipe extends recipeModule_1.default { deviceId: codeInfo.deviceId, userInputCode: codeInfo.userInputCode, tenantId: input.tenantId, + shouldAttemptAccountLinkingIfAllowed: input.shouldAttemptAccountLinkingIfAllowed, userContext: input.userContext, } ); @@ -183,7 +188,6 @@ class Recipe extends recipeModule_1.default { createdNewRecipeUser: consumeCodeResponse.createdNewRecipeUser, recipeUserId: consumeCodeResponse.recipeUserId, user: consumeCodeResponse.user, - isValidFirstFactorForTenant: consumeCodeResponse.isValidFirstFactorForTenant, }; } else { throw new Error("Failed to create user. Please retry"); @@ -227,6 +231,53 @@ class Recipe extends recipeModule_1.default { emailDelivery: undefined, smsDelivery: undefined, }); + let otpOrLink = []; + let emailOrPhone = []; + if (Recipe.instance.config.flowType === "MAGIC_LINK") { + otpOrLink.push("link"); + } else if (Recipe.instance.config.flowType === "USER_INPUT_CODE") { + otpOrLink.push("otp"); + } else { + otpOrLink.push("otp"); + otpOrLink.push("link"); + } + if (Recipe.instance.config.contactMethod === "EMAIL") { + emailOrPhone.push("email"); + } else if (Recipe.instance.config.contactMethod === "PHONE") { + emailOrPhone.push("phone"); + } else { + emailOrPhone.push("email"); + emailOrPhone.push("phone"); + } + const allFactors = []; + for (const ol of otpOrLink) { + for (const ep of emailOrPhone) { + allFactors.push(`${ol}-${ep}`); + } + } + postSuperTokensInitCallbacks_1.PostSuperTokensInitCallbacks.addPostInitCallback(() => { + const mfaInstance = recipe_1.default.getInstance(); + if (mfaInstance !== undefined) { + mfaInstance.addGetAllFactorsFromOtherRecipesFunc((tenantConfig) => { + if (tenantConfig.passwordless.enabled === false) { + return { + factorIds: [], + firstFactorIds: [], + }; + } + return { + factorIds: allFactors, + firstFactorIds: allFactors, + }; + }); + mfaInstance.addGetFactorsSetupForUserFromOtherRecipes(async (user, tenantConfig) => { + if (tenantConfig.passwordless.enabled === false) { + return []; + } + return allFactors.filter((id) => utils_2.isFactorSetupForUser(user, id)); + }); + } + }); return Recipe.instance; } else { throw new Error("Passwordless recipe has already been initialised. Please check your code for bugs."); diff --git a/lib/build/recipe/passwordless/recipeImplementation.js b/lib/build/recipe/passwordless/recipeImplementation.js index 15423fdf6..11fad8247 100644 --- a/lib/build/recipe/passwordless/recipeImplementation.js +++ b/lib/build/recipe/passwordless/recipeImplementation.js @@ -23,6 +23,7 @@ function getRecipeInterface(querier) { } return { consumeCode: async function (input) { + var _a; let response = await querier.sendPostRequest( new normalisedURLPath_1.default(`/${input.tenantId}/recipe/signinup/code/consume`), copyAndRemoveUserContextAndTenantId(input), @@ -55,14 +56,16 @@ function getRecipeInterface(querier) { createdNewRecipeUser: response.createdNewUser, user: response.user, recipeUserId: response.recipeUserId, - isValidFirstFactorForTenant: response.isValidFirstFactorForTenant, }; } - let updatedUser = await recipe_1.default.getInstance().createPrimaryUserIdOrLinkAccounts({ - tenantId: input.tenantId, - user: response.user, - userContext: input.userContext, - }); + let updatedUser = response.user; + if ((_a = input.shouldAttemptAccountLinkingIfAllowed) !== null && _a !== void 0 ? _a : true) { + updatedUser = await recipe_1.default.getInstance().createPrimaryUserIdOrLinkAccounts({ + tenantId: input.tenantId, + user: response.user, + userContext: input.userContext, + }); + } if (updatedUser === undefined) { throw new Error("Should never come here."); } @@ -71,7 +74,6 @@ function getRecipeInterface(querier) { createdNewRecipeUser: response.createdNewUser, user: updatedUser, recipeUserId: response.recipeUserId, - isValidFirstFactorForTenant: response.isValidFirstFactorForTenant, }; }, createCode: async function (input) { diff --git a/lib/build/recipe/passwordless/smsdelivery/services/backwardCompatibility/index.d.ts b/lib/build/recipe/passwordless/smsdelivery/services/backwardCompatibility/index.d.ts index 66c9c0498..a0e2dbe84 100644 --- a/lib/build/recipe/passwordless/smsdelivery/services/backwardCompatibility/index.d.ts +++ b/lib/build/recipe/passwordless/smsdelivery/services/backwardCompatibility/index.d.ts @@ -1,11 +1,12 @@ // @ts-nocheck import { TypePasswordlessSmsDeliveryInput } from "../../../types"; import { SmsDeliveryInterface } from "../../../../../ingredients/smsdelivery/types"; +import { UserContext } from "../../../../../types"; export default class BackwardCompatibilityService implements SmsDeliveryInterface { constructor(); sendSms: ( input: TypePasswordlessSmsDeliveryInput & { - userContext: any; + userContext: UserContext; } ) => Promise; } diff --git a/lib/build/recipe/passwordless/smsdelivery/services/twilio/index.d.ts b/lib/build/recipe/passwordless/smsdelivery/services/twilio/index.d.ts index ef7c09e1d..d5e84942d 100644 --- a/lib/build/recipe/passwordless/smsdelivery/services/twilio/index.d.ts +++ b/lib/build/recipe/passwordless/smsdelivery/services/twilio/index.d.ts @@ -2,13 +2,14 @@ import { ServiceInterface, TypeInput } from "../../../../../ingredients/smsdelivery/services/twilio"; import { SmsDeliveryInterface } from "../../../../../ingredients/smsdelivery/types"; import { TypePasswordlessSmsDeliveryInput } from "../../../types"; +import { UserContext } from "../../../../../types"; export default class TwilioService implements SmsDeliveryInterface { serviceImpl: ServiceInterface; private config; constructor(config: TypeInput); sendSms: ( input: TypePasswordlessSmsDeliveryInput & { - userContext: any; + userContext: UserContext; } ) => Promise; } diff --git a/lib/build/recipe/passwordless/types.d.ts b/lib/build/recipe/passwordless/types.d.ts index 97fe81dd9..2ad4aac83 100644 --- a/lib/build/recipe/passwordless/types.d.ts +++ b/lib/build/recipe/passwordless/types.d.ts @@ -12,8 +12,9 @@ import { TypeInputWithService as SmsDeliveryTypeInputWithService, } from "../../ingredients/smsdelivery/types"; import SmsDeliveryIngredient from "../../ingredients/smsdelivery"; -import { GeneralErrorResponse, NormalisedAppinfo, User } from "../../types"; +import { GeneralErrorResponse, NormalisedAppinfo, User, UserContext } from "../../types"; import RecipeUserId from "../../recipeUserId"; +import { MFAFlowErrors } from "../multifactorauth/types"; export declare type TypeInput = ( | { contactMethod: "PHONE"; @@ -38,7 +39,7 @@ export declare type TypeInput = ( flowType: "USER_INPUT_CODE" | "MAGIC_LINK" | "USER_INPUT_CODE_AND_MAGIC_LINK"; emailDelivery?: EmailDeliveryTypeInput; smsDelivery?: SmsDeliveryTypeInput; - getCustomUserInputCode?: (tenantId: string, userContext: any) => Promise | string; + getCustomUserInputCode?: (tenantId: string, userContext: UserContext) => Promise | string; override?: { functions?: ( originalImplementation: RecipeInterface, @@ -69,7 +70,7 @@ export declare type TypeNormalisedInput = ( } ) & { flowType: "USER_INPUT_CODE" | "MAGIC_LINK" | "USER_INPUT_CODE_AND_MAGIC_LINK"; - getCustomUserInputCode?: (tenantId: string, userContext: any) => Promise | string; + getCustomUserInputCode?: (tenantId: string, userContext: UserContext) => Promise | string; getSmsDeliveryConfig: () => SmsDeliveryTypeInputWithService; getEmailDeliveryConfig: () => EmailDeliveryTypeInputWithService; override: { @@ -92,7 +93,7 @@ export declare type RecipeInterface = { ) & { userInputCode?: string; tenantId: string; - userContext: any; + userContext: UserContext; } ) => Promise<{ status: "OK"; @@ -108,7 +109,7 @@ export declare type RecipeInterface = { deviceId: string; userInputCode?: string; tenantId: string; - userContext: any; + userContext: UserContext; }) => Promise< | { status: "OK"; @@ -131,13 +132,15 @@ export declare type RecipeInterface = { deviceId: string; preAuthSessionId: string; tenantId: string; - userContext: any; + shouldAttemptAccountLinkingIfAllowed?: boolean; + userContext: UserContext; } | { linkCode: string; preAuthSessionId: string; tenantId: string; - userContext: any; + shouldAttemptAccountLinkingIfAllowed?: boolean; + userContext: UserContext; } ) => Promise< | { @@ -145,7 +148,6 @@ export declare type RecipeInterface = { createdNewRecipeUser: boolean; user: User; recipeUserId: RecipeUserId; - isValidFirstFactorForTenant: boolean | undefined; } | { status: "INCORRECT_USER_INPUT_CODE_ERROR" | "EXPIRED_USER_INPUT_CODE_ERROR"; @@ -160,7 +162,7 @@ export declare type RecipeInterface = { recipeUserId: RecipeUserId; email?: string | null; phoneNumber?: string | null; - userContext: any; + userContext: UserContext; }) => Promise< | { status: @@ -179,12 +181,12 @@ export declare type RecipeInterface = { | { email: string; tenantId: string; - userContext: any; + userContext: UserContext; } | { phoneNumber: string; tenantId: string; - userContext: any; + userContext: UserContext; } ) => Promise<{ status: "OK"; @@ -192,25 +194,25 @@ export declare type RecipeInterface = { revokeCode: (input: { codeId: string; tenantId: string; - userContext: any; + userContext: UserContext; }) => Promise<{ status: "OK"; }>; - listCodesByEmail: (input: { email: string; tenantId: string; userContext: any }) => Promise; + listCodesByEmail: (input: { email: string; tenantId: string; userContext: UserContext }) => Promise; listCodesByPhoneNumber: (input: { phoneNumber: string; tenantId: string; - userContext: any; + userContext: UserContext; }) => Promise; listCodesByDeviceId: (input: { deviceId: string; tenantId: string; - userContext: any; + userContext: UserContext; }) => Promise; listCodesByPreAuthSessionId: (input: { preAuthSessionId: string; tenantId: string; - userContext: any; + userContext: UserContext; }) => Promise; }; export declare type DeviceType = { @@ -247,7 +249,7 @@ export declare type APIInterface = { ) & { tenantId: string; options: APIOptions; - userContext: any; + userContext: UserContext; } ) => Promise< | { @@ -269,7 +271,7 @@ export declare type APIInterface = { } & { tenantId: string; options: APIOptions; - userContext: any; + userContext: UserContext; } ) => Promise< | GeneralErrorResponse @@ -291,7 +293,7 @@ export declare type APIInterface = { ) & { tenantId: string; options: APIOptions; - userContext: any; + userContext: UserContext; } ) => Promise< | { @@ -305,7 +307,6 @@ export declare type APIInterface = { failedCodeInputAttemptCount: number; maximumCodeInputAttempts: number; } - | GeneralErrorResponse | { status: "RESTART_FLOW_ERROR"; } @@ -313,12 +314,14 @@ export declare type APIInterface = { status: "SIGN_IN_UP_NOT_ALLOWED"; reason: string; } + | GeneralErrorResponse + | MFAFlowErrors >; emailExistsGET?: (input: { email: string; tenantId: string; options: APIOptions; - userContext: any; + userContext: UserContext; }) => Promise< | { status: "OK"; @@ -330,7 +333,7 @@ export declare type APIInterface = { phoneNumber: string; tenantId: string; options: APIOptions; - userContext: any; + userContext: UserContext; }) => Promise< | { status: "OK"; diff --git a/lib/build/recipe/passwordless/utils.d.ts b/lib/build/recipe/passwordless/utils.d.ts index f00fc5184..b3b7193c3 100644 --- a/lib/build/recipe/passwordless/utils.d.ts +++ b/lib/build/recipe/passwordless/utils.d.ts @@ -2,6 +2,7 @@ import Recipe from "./recipe"; import { TypeInput, TypeNormalisedInput } from "./types"; import { NormalisedAppinfo } from "../../types"; +import { User } from "../../user"; export declare function validateAndNormaliseUserInput( _: Recipe, appInfo: NormalisedAppinfo, @@ -9,3 +10,4 @@ export declare function validateAndNormaliseUserInput( ): TypeNormalisedInput; export declare function defaultValidatePhoneNumber(value: string): Promise | string | undefined; export declare function defaultValidateEmail(value: string): Promise | string | undefined; +export declare function isFactorSetupForUser(user: User, factorId: string): boolean; diff --git a/lib/build/recipe/passwordless/utils.js b/lib/build/recipe/passwordless/utils.js index 8e8a98fd4..482ea47da 100644 --- a/lib/build/recipe/passwordless/utils.js +++ b/lib/build/recipe/passwordless/utils.js @@ -19,7 +19,7 @@ var __importDefault = return mod && mod.__esModule ? mod : { default: mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); -exports.defaultValidateEmail = exports.defaultValidatePhoneNumber = exports.validateAndNormaliseUserInput = void 0; +exports.isFactorSetupForUser = exports.defaultValidateEmail = exports.defaultValidatePhoneNumber = exports.validateAndNormaliseUserInput = void 0; const max_1 = __importDefault(require("libphonenumber-js/max")); const backwardCompatibility_1 = __importDefault(require("./emaildelivery/services/backwardCompatibility")); const backwardCompatibility_2 = __importDefault(require("./smsdelivery/services/backwardCompatibility")); @@ -164,3 +164,25 @@ function defaultValidateEmail(value) { return undefined; } exports.defaultValidateEmail = defaultValidateEmail; +function isFactorSetupForUser(user, factorId) { + for (const loginMethod of user.loginMethods) { + if (loginMethod.email !== undefined) { + if (factorId == "otp-email") { + return true; + } + if (factorId == "link-email") { + return true; + } + } + if (loginMethod.phoneNumber !== undefined) { + if (factorId == "otp-phone") { + return true; + } + if (factorId == "link-phone") { + return true; + } + } + } + return false; +} +exports.isFactorSetupForUser = isFactorSetupForUser; diff --git a/lib/build/recipe/session/api/refresh.d.ts b/lib/build/recipe/session/api/refresh.d.ts index 6694f3d42..63910c21a 100644 --- a/lib/build/recipe/session/api/refresh.d.ts +++ b/lib/build/recipe/session/api/refresh.d.ts @@ -1,7 +1,8 @@ // @ts-nocheck import { APIInterface, APIOptions } from "../"; +import { UserContext } from "../../../types"; export default function handleRefreshAPI( apiImplementation: APIInterface, options: APIOptions, - userContext: any + userContext: UserContext ): Promise; diff --git a/lib/build/recipe/session/api/signout.d.ts b/lib/build/recipe/session/api/signout.d.ts index 8a0959b32..758ce9ed6 100644 --- a/lib/build/recipe/session/api/signout.d.ts +++ b/lib/build/recipe/session/api/signout.d.ts @@ -1,7 +1,8 @@ // @ts-nocheck import { APIInterface, APIOptions } from "../"; +import { UserContext } from "../../../types"; export default function signOutAPI( apiImplementation: APIInterface, options: APIOptions, - userContext: any + userContext: UserContext ): Promise; diff --git a/lib/build/recipe/session/claimBaseClasses/primitiveArrayClaim.d.ts b/lib/build/recipe/session/claimBaseClasses/primitiveArrayClaim.d.ts index 1aa051606..a378bf4c3 100644 --- a/lib/build/recipe/session/claimBaseClasses/primitiveArrayClaim.d.ts +++ b/lib/build/recipe/session/claimBaseClasses/primitiveArrayClaim.d.ts @@ -1,21 +1,22 @@ // @ts-nocheck import RecipeUserId from "../../../recipeUserId"; -import { JSONPrimitive } from "../../../types"; +import { JSONObject, JSONPrimitive, UserContext } from "../../../types"; import { SessionClaim, SessionClaimValidator } from "../types"; export declare class PrimitiveArrayClaim extends SessionClaim { readonly fetchValue: ( userId: string, recipeUserId: RecipeUserId, tenantId: string, - userContext: any + currentPayload: JSONObject | undefined, + userContext: UserContext ) => Promise | T[] | undefined; readonly defaultMaxAgeInSeconds: number | undefined; constructor(config: { key: string; fetchValue: SessionClaim["fetchValue"]; defaultMaxAgeInSeconds?: number }); - addToPayload_internal(payload: any, value: T[], _userContext: any): any; - removeFromPayloadByMerge_internal(payload: any, _userContext?: any): any; - removeFromPayload(payload: any, _userContext?: any): any; - getValueFromPayload(payload: any, _userContext?: any): T[] | undefined; - getLastRefetchTime(payload: any, _userContext?: any): number | undefined; + addToPayload_internal(payload: any, value: T[], _userContext: UserContext): any; + removeFromPayloadByMerge_internal(payload: any, _userContext?: UserContext): any; + removeFromPayload(payload: any, _userContext?: UserContext): any; + getValueFromPayload(payload: any, _userContext?: UserContext): T[] | undefined; + getLastRefetchTime(payload: any, _userContext?: UserContext): number | undefined; validators: { includes: (val: T, maxAgeInSeconds?: number | undefined, id?: string | undefined) => SessionClaimValidator; excludes: (val: T, maxAgeInSeconds?: number | undefined, id?: string | undefined) => SessionClaimValidator; diff --git a/lib/build/recipe/session/claimBaseClasses/primitiveClaim.d.ts b/lib/build/recipe/session/claimBaseClasses/primitiveClaim.d.ts index 8ecb30c81..e3ec38931 100644 --- a/lib/build/recipe/session/claimBaseClasses/primitiveClaim.d.ts +++ b/lib/build/recipe/session/claimBaseClasses/primitiveClaim.d.ts @@ -1,21 +1,22 @@ // @ts-nocheck import RecipeUserId from "../../../recipeUserId"; -import { JSONPrimitive } from "../../../types"; +import { JSONObject, JSONPrimitive, UserContext } from "../../../types"; import { SessionClaim, SessionClaimValidator } from "../types"; export declare class PrimitiveClaim extends SessionClaim { readonly fetchValue: ( userId: string, recipeUserId: RecipeUserId, tenantId: string, - userContext: any + currentPayload: JSONObject | undefined, + userContext: UserContext ) => Promise | T | undefined; readonly defaultMaxAgeInSeconds: number | undefined; constructor(config: { key: string; fetchValue: SessionClaim["fetchValue"]; defaultMaxAgeInSeconds?: number }); - addToPayload_internal(payload: any, value: T, _userContext: any): any; - removeFromPayloadByMerge_internal(payload: any, _userContext?: any): any; - removeFromPayload(payload: any, _userContext?: any): any; - getValueFromPayload(payload: any, _userContext?: any): T | undefined; - getLastRefetchTime(payload: any, _userContext?: any): number | undefined; + addToPayload_internal(payload: any, value: T, _userContext: UserContext): any; + removeFromPayloadByMerge_internal(payload: any, _userContext?: UserContext): any; + removeFromPayload(payload: any, _userContext?: UserContext): any; + getValueFromPayload(payload: any, _userContext?: UserContext): T | undefined; + getLastRefetchTime(payload: any, _userContext?: UserContext): number | undefined; validators: { hasValue: (val: T, maxAgeInSeconds?: number | undefined, id?: string | undefined) => SessionClaimValidator; }; diff --git a/lib/build/recipe/session/cookieAndHeaders.d.ts b/lib/build/recipe/session/cookieAndHeaders.d.ts index 40b15126f..9197f5325 100644 --- a/lib/build/recipe/session/cookieAndHeaders.d.ts +++ b/lib/build/recipe/session/cookieAndHeaders.d.ts @@ -1,18 +1,19 @@ // @ts-nocheck import type { BaseRequest, BaseResponse } from "../../framework"; +import { UserContext } from "../../types"; import { TokenTransferMethod, TokenType, TypeNormalisedInput } from "./types"; export declare function clearSessionFromAllTokenTransferMethods( config: TypeNormalisedInput, res: BaseResponse, request: BaseRequest | undefined, - userContext: any + userContext: UserContext ): void; export declare function clearSession( config: TypeNormalisedInput, res: BaseResponse, transferMethod: TokenTransferMethod, request: BaseRequest | undefined, - userContext: any + userContext: UserContext ): void; export declare function getAntiCsrfTokenFromHeaders(req: BaseRequest): string | undefined; export declare function setAntiCsrfTokenInHeaders(res: BaseResponse, antiCsrfToken: string): void; @@ -32,7 +33,7 @@ export declare function setToken( expires: number, transferMethod: TokenTransferMethod, req: BaseRequest | undefined, - userContext: any + userContext: UserContext ): void; export declare function setHeader(res: BaseResponse, name: string, value: string): void; /** @@ -54,6 +55,6 @@ export declare function setCookie( expires: number, pathType: "refreshTokenPath" | "accessTokenPath", req: BaseRequest | undefined, - userContext: any + userContext: UserContext ): void; export declare function getAuthModeFromHeader(req: BaseRequest): string | undefined; diff --git a/lib/build/recipe/session/index.d.ts b/lib/build/recipe/session/index.d.ts index 3f403f4f6..62a069d67 100644 --- a/lib/build/recipe/session/index.d.ts +++ b/lib/build/recipe/session/index.d.ts @@ -24,7 +24,16 @@ export default class SessionWrapper { recipeUserId: RecipeUserId, accessTokenPayload?: any, sessionDataInDatabase?: any, - userContext?: any + userContext?: Record + ): Promise; + static createNewOrKeepExistingSession( + req: any, + res: any, + tenantId: string, + recipeUserId: RecipeUserId, + accessTokenPayload?: any, + sessionDataInDatabase?: any, + userContext?: Record ): Promise; static createNewSessionWithoutRequestResponse( tenantId: string, @@ -32,16 +41,16 @@ export default class SessionWrapper { accessTokenPayload?: any, sessionDataInDatabase?: any, disableAntiCsrf?: boolean, - userContext?: any + userContext?: Record ): Promise; static validateClaimsForSessionHandle( sessionHandle: string, overrideGlobalClaimValidators?: ( globalClaimValidators: SessionClaimValidator[], sessionInfo: SessionInformation, - userContext: any + userContext: Record ) => Promise | SessionClaimValidator[], - userContext?: any + userContext?: Record ): Promise< | { status: "SESSION_DOES_NOT_EXIST_ERROR"; @@ -58,7 +67,7 @@ export default class SessionWrapper { options?: VerifySessionOptions & { sessionRequired?: true; }, - userContext?: any + userContext?: Record ): Promise; static getSession( req: any, @@ -66,13 +75,13 @@ export default class SessionWrapper { options?: VerifySessionOptions & { sessionRequired: false; }, - userContext?: any + userContext?: Record ): Promise; static getSession( req: any, res: any, options?: VerifySessionOptions, - userContext?: any + userContext?: Record ): Promise; /** * Tries to validate an access token and build a Session object from it. @@ -101,7 +110,7 @@ export default class SessionWrapper { options?: VerifySessionOptions & { sessionRequired?: true; }, - userContext?: any + userContext?: Record ): Promise; static getSessionWithoutRequestResponse( accessToken: string, @@ -109,47 +118,54 @@ export default class SessionWrapper { options?: VerifySessionOptions & { sessionRequired: false; }, - userContext?: any + userContext?: Record ): Promise; static getSessionWithoutRequestResponse( accessToken: string, antiCsrfToken?: string, options?: VerifySessionOptions, - userContext?: any + userContext?: Record ): Promise; - static getSessionInformation(sessionHandle: string, userContext?: any): Promise; - static refreshSession(req: any, res: any, userContext?: any): Promise; + static getSessionInformation( + sessionHandle: string, + userContext?: Record + ): Promise; + static refreshSession(req: any, res: any, userContext?: Record): Promise; static refreshSessionWithoutRequestResponse( refreshToken: string, disableAntiCsrf?: boolean, antiCsrfToken?: string, - userContext?: any + userContext?: Record ): Promise; static revokeAllSessionsForUser( userId: string, revokeSessionsForLinkedAccounts?: boolean, tenantId?: string, - userContext?: any + userContext?: Record ): Promise; static getAllSessionHandlesForUser( userId: string, fetchSessionsForAllLinkedAccounts?: boolean, tenantId?: string, - userContext?: any + userContext?: Record ): Promise; - static revokeSession(sessionHandle: string, userContext?: any): Promise; - static revokeMultipleSessions(sessionHandles: string[], userContext?: any): Promise; - static updateSessionDataInDatabase(sessionHandle: string, newSessionData: any, userContext?: any): Promise; + static revokeSession(sessionHandle: string, userContext?: Record): Promise; + static revokeMultipleSessions(sessionHandles: string[], userContext?: Record): Promise; + static updateSessionDataInDatabase( + sessionHandle: string, + newSessionData: any, + userContext?: Record + ): Promise; static mergeIntoAccessTokenPayload( sessionHandle: string, accessTokenPayloadUpdate: JSONObject, - userContext?: any + userContext?: Record ): Promise; static createJWT( payload?: any, validitySeconds?: number, useStaticSigningKey?: boolean, - userContext?: any + userContext?: Record ): Promise< | { status: "OK"; @@ -160,28 +176,32 @@ export default class SessionWrapper { } >; static getJWKS( - userContext?: any + userContext?: Record ): Promise<{ keys: import("../jwt").JsonWebKey[]; }>; static getOpenIdDiscoveryConfiguration( - userContext?: any + userContext?: Record ): Promise<{ status: "OK"; issuer: string; jwks_uri: string; }>; - static fetchAndSetClaim(sessionHandle: string, claim: SessionClaim, userContext?: any): Promise; + static fetchAndSetClaim( + sessionHandle: string, + claim: SessionClaim, + userContext?: Record + ): Promise; static setClaimValue( sessionHandle: string, claim: SessionClaim, value: T, - userContext?: any + userContext?: Record ): Promise; static getClaimValue( sessionHandle: string, claim: SessionClaim, - userContext?: any + userContext?: Record ): Promise< | { status: "SESSION_DOES_NOT_EXIST_ERROR"; @@ -191,7 +211,11 @@ export default class SessionWrapper { value: T | undefined; } >; - static removeClaim(sessionHandle: string, claim: SessionClaim, userContext?: any): Promise; + static removeClaim( + sessionHandle: string, + claim: SessionClaim, + userContext?: Record + ): Promise; } export declare let init: typeof Recipe.init; export declare let createNewSession: typeof SessionWrapper.createNewSession; diff --git a/lib/build/recipe/session/index.js b/lib/build/recipe/session/index.js index 82b519987..4171174ed 100644 --- a/lib/build/recipe/session/index.js +++ b/lib/build/recipe/session/index.js @@ -27,6 +27,7 @@ const sessionRequestFunctions_1 = require("./sessionRequestFunctions"); const __1 = require("../.."); const constants_1 = require("../multitenancy/constants"); const constants_2 = require("./constants"); +const utils_2 = require("../../utils"); class SessionWrapper { static async createNewSession( req, @@ -35,7 +36,7 @@ class SessionWrapper { recipeUserId, accessTokenPayload = {}, sessionDataInDatabase = {}, - userContext = {} + userContext ) { const recipeInstance = recipe_1.default.getInstanceOrThrowError(); const config = recipeInstance.config; @@ -48,7 +49,7 @@ class SessionWrapper { return await sessionRequestFunctions_1.createNewSessionInRequest({ req, res, - userContext, + userContext: utils_2.getUserContext(userContext), recipeInstance, accessTokenPayload, userId, @@ -59,14 +60,46 @@ class SessionWrapper { tenantId, }); } + static async createNewOrKeepExistingSession( + req, + res, + tenantId, + recipeUserId, + accessTokenPayload = {}, + sessionDataInDatabase = {}, + userContext + ) { + const ctx = utils_2.getUserContext(userContext); + const recipeInstance = recipe_1.default.getInstanceOrThrowError(); + const config = recipeInstance.config; + let session = await exports.getSession( + req, + req, + { sessionRequired: false, overrideGlobalClaimValidators: () => [] }, + ctx + ); + if (session === undefined || config.overwriteSessionDuringSignIn) { + session = await exports.createNewSession( + req, + res, + tenantId, + recipeUserId, + accessTokenPayload, + sessionDataInDatabase, + ctx + ); + } + return session; + } static async createNewSessionWithoutRequestResponse( tenantId, recipeUserId, accessTokenPayload = {}, sessionDataInDatabase = {}, disableAntiCsrf = false, - userContext = {} + userContext ) { + const ctx = utils_2.getUserContext(userContext); const recipeInstance = recipe_1.default.getInstanceOrThrowError(); const claimsAddedByOtherRecipes = recipeInstance.getClaimsAddedByOtherRecipes(); const appInfo = recipeInstance.getAppInfo(); @@ -75,13 +108,13 @@ class SessionWrapper { for (const prop of constants_2.protectedProps) { delete finalAccessTokenPayload[prop]; } - let user = await __1.getUser(recipeUserId.getAsString(), userContext); + let user = await __1.getUser(recipeUserId.getAsString(), ctx); let userId = recipeUserId.getAsString(); if (user !== undefined) { userId = user.id; } for (const claim of claimsAddedByOtherRecipes) { - const update = await claim.build(userId, recipeUserId, tenantId, userContext); + const update = await claim.build(userId, recipeUserId, tenantId, finalAccessTokenPayload, ctx); finalAccessTokenPayload = Object.assign(Object.assign({}, finalAccessTokenPayload), update); } return recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.createNewSession({ @@ -91,14 +124,15 @@ class SessionWrapper { sessionDataInDatabase, disableAntiCsrf, tenantId, - userContext, + userContext: ctx, }); } - static async validateClaimsForSessionHandle(sessionHandle, overrideGlobalClaimValidators, userContext = {}) { + static async validateClaimsForSessionHandle(sessionHandle, overrideGlobalClaimValidators, userContext) { + const ctx = utils_2.getUserContext(userContext); const recipeImpl = recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl; const sessionInfo = await recipeImpl.getSessionInformation({ sessionHandle, - userContext, + userContext: ctx, }); if (sessionInfo === undefined) { return { @@ -113,25 +147,25 @@ class SessionWrapper { recipeUserId: sessionInfo.recipeUserId, tenantId: sessionInfo.tenantId, claimValidatorsAddedByOtherRecipes, - userContext, + userContext: ctx, }); const claimValidators = overrideGlobalClaimValidators !== undefined - ? await overrideGlobalClaimValidators(globalClaimValidators, sessionInfo, userContext) + ? await overrideGlobalClaimValidators(globalClaimValidators, sessionInfo, ctx) : globalClaimValidators; let claimValidationResponse = await recipeImpl.validateClaims({ userId: sessionInfo.userId, recipeUserId: sessionInfo.recipeUserId, accessTokenPayload: sessionInfo.customClaimsInAccessTokenPayload, claimValidators, - userContext, + userContext: ctx, }); if (claimValidationResponse.accessTokenPayloadUpdate !== undefined) { if ( !(await recipeImpl.mergeIntoAccessTokenPayload({ sessionHandle, accessTokenPayloadUpdate: claimValidationResponse.accessTokenPayloadUpdate, - userContext, + userContext: ctx, })) ) { return { @@ -154,146 +188,144 @@ class SessionWrapper { recipeInterfaceImpl, config, options, - userContext, // userContext is normalized inside the function + userContext: utils_2.getUserContext(userContext), // userContext is normalized inside the function }); } - static async getSessionWithoutRequestResponse(accessToken, antiCsrfToken, options, userContext = {}) { + static async getSessionWithoutRequestResponse(accessToken, antiCsrfToken, options, userContext) { + const ctx = utils_2.getUserContext(userContext); const recipeInterfaceImpl = recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl; const session = await recipeInterfaceImpl.getSession({ accessToken, antiCsrfToken, options, - userContext, + userContext: ctx, }); if (session !== undefined) { const claimValidators = await utils_1.getRequiredClaimValidators( session, options === null || options === void 0 ? void 0 : options.overrideGlobalClaimValidators, - userContext + ctx ); - await session.assertClaims(claimValidators, userContext); + await session.assertClaims(claimValidators, ctx); } return session; } - static getSessionInformation(sessionHandle, userContext = {}) { + static getSessionInformation(sessionHandle, userContext) { return recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.getSessionInformation({ sessionHandle, - userContext, + userContext: utils_2.getUserContext(userContext), }); } - static refreshSession(req, res, userContext = {}) { + static refreshSession(req, res, userContext) { const recipeInstance = recipe_1.default.getInstanceOrThrowError(); const config = recipeInstance.config; const recipeInterfaceImpl = recipeInstance.recipeInterfaceImpl; return sessionRequestFunctions_1.refreshSessionInRequest({ res, req, - userContext, + userContext: utils_2.getUserContext(userContext), config, recipeInterfaceImpl, }); } - static refreshSessionWithoutRequestResponse( - refreshToken, - disableAntiCsrf = false, - antiCsrfToken, - userContext = {} - ) { + static refreshSessionWithoutRequestResponse(refreshToken, disableAntiCsrf = false, antiCsrfToken, userContext) { return recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.refreshSession({ refreshToken, disableAntiCsrf, antiCsrfToken, - userContext, + userContext: utils_2.getUserContext(userContext), }); } - static revokeAllSessionsForUser(userId, revokeSessionsForLinkedAccounts = true, tenantId, userContext = {}) { + static revokeAllSessionsForUser(userId, revokeSessionsForLinkedAccounts = true, tenantId, userContext) { return recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.revokeAllSessionsForUser({ userId, tenantId: tenantId === undefined ? constants_1.DEFAULT_TENANT_ID : tenantId, revokeSessionsForLinkedAccounts, - userContext, + userContext: utils_2.getUserContext(userContext), }); } - static getAllSessionHandlesForUser(userId, fetchSessionsForAllLinkedAccounts = true, tenantId, userContext = {}) { + static getAllSessionHandlesForUser(userId, fetchSessionsForAllLinkedAccounts = true, tenantId, userContext) { return recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.getAllSessionHandlesForUser({ userId, tenantId: tenantId === undefined ? constants_1.DEFAULT_TENANT_ID : tenantId, fetchAcrossAllTenants: tenantId === undefined, fetchSessionsForAllLinkedAccounts, - userContext, + userContext: utils_2.getUserContext(userContext), }); } - static revokeSession(sessionHandle, userContext = {}) { + static revokeSession(sessionHandle, userContext) { return recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.revokeSession({ sessionHandle, - userContext, + userContext: utils_2.getUserContext(userContext), }); } - static revokeMultipleSessions(sessionHandles, userContext = {}) { + static revokeMultipleSessions(sessionHandles, userContext) { return recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.revokeMultipleSessions({ sessionHandles, - userContext, + userContext: utils_2.getUserContext(userContext), }); } - static updateSessionDataInDatabase(sessionHandle, newSessionData, userContext = {}) { + static updateSessionDataInDatabase(sessionHandle, newSessionData, userContext) { return recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.updateSessionDataInDatabase({ sessionHandle, newSessionData, - userContext, + userContext: utils_2.getUserContext(userContext), }); } - static mergeIntoAccessTokenPayload(sessionHandle, accessTokenPayloadUpdate, userContext = {}) { + static mergeIntoAccessTokenPayload(sessionHandle, accessTokenPayloadUpdate, userContext) { return recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.mergeIntoAccessTokenPayload({ sessionHandle, accessTokenPayloadUpdate, - userContext, + userContext: utils_2.getUserContext(userContext), }); } - static createJWT(payload, validitySeconds, useStaticSigningKey, userContext = {}) { + static createJWT(payload, validitySeconds, useStaticSigningKey, userContext) { return recipe_1.default.getInstanceOrThrowError().openIdRecipe.recipeImplementation.createJWT({ payload, validitySeconds, useStaticSigningKey, - userContext, + userContext: utils_2.getUserContext(userContext), }); } - static getJWKS(userContext = {}) { - return recipe_1.default.getInstanceOrThrowError().openIdRecipe.recipeImplementation.getJWKS({ userContext }); + static getJWKS(userContext) { + return recipe_1.default.getInstanceOrThrowError().openIdRecipe.recipeImplementation.getJWKS({ + userContext: utils_2.getUserContext(userContext), + }); } - static getOpenIdDiscoveryConfiguration(userContext = {}) { + static getOpenIdDiscoveryConfiguration(userContext) { return recipe_1.default .getInstanceOrThrowError() .openIdRecipe.recipeImplementation.getOpenIdDiscoveryConfiguration({ - userContext, + userContext: utils_2.getUserContext(userContext), }); } - static fetchAndSetClaim(sessionHandle, claim, userContext = {}) { + static fetchAndSetClaim(sessionHandle, claim, userContext) { return recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.fetchAndSetClaim({ sessionHandle, claim, - userContext, + userContext: utils_2.getUserContext(userContext), }); } - static setClaimValue(sessionHandle, claim, value, userContext = {}) { + static setClaimValue(sessionHandle, claim, value, userContext) { return recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.setClaimValue({ sessionHandle, claim, value, - userContext, + userContext: utils_2.getUserContext(userContext), }); } - static getClaimValue(sessionHandle, claim, userContext = {}) { + static getClaimValue(sessionHandle, claim, userContext) { return recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.getClaimValue({ sessionHandle, claim, - userContext, + userContext: utils_2.getUserContext(userContext), }); } - static removeClaim(sessionHandle, claim, userContext = {}) { + static removeClaim(sessionHandle, claim, userContext) { return recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.removeClaim({ sessionHandle, claim, - userContext, + userContext: utils_2.getUserContext(userContext), }); } } diff --git a/lib/build/recipe/session/recipe.d.ts b/lib/build/recipe/session/recipe.d.ts index 678d36663..0489b1e81 100644 --- a/lib/build/recipe/session/recipe.d.ts +++ b/lib/build/recipe/session/recipe.d.ts @@ -10,7 +10,7 @@ import { SessionClaim, } from "./types"; import STError from "./error"; -import { NormalisedAppinfo, RecipeListFunction, APIHandled, HTTPMethod } from "../../types"; +import { NormalisedAppinfo, RecipeListFunction, APIHandled, HTTPMethod, UserContext } from "../../types"; import NormalisedURLPath from "../../normalisedURLPath"; import type { BaseRequest, BaseResponse } from "../../framework"; import OpenIdRecipe from "../openid/recipe"; @@ -40,15 +40,20 @@ export default class SessionRecipe extends RecipeModule { res: BaseResponse, path: NormalisedURLPath, method: HTTPMethod, - userContext: any + userContext: UserContext ) => Promise; - handleError: (err: STError, request: BaseRequest, response: BaseResponse, userContext: any) => Promise; + handleError: ( + err: STError, + request: BaseRequest, + response: BaseResponse, + userContext: UserContext + ) => Promise; getAllCORSHeaders: () => string[]; isErrorFromThisRecipe: (err: any) => err is STError; verifySession: ( options: VerifySessionOptions | undefined, request: BaseRequest, response: BaseResponse, - userContext: any + userContext: UserContext ) => Promise; } diff --git a/lib/build/recipe/session/recipe.js b/lib/build/recipe/session/recipe.js index ab82761bb..55792d9b9 100644 --- a/lib/build/recipe/session/recipe.js +++ b/lib/build/recipe/session/recipe.js @@ -110,10 +110,15 @@ class SessionRecipe extends recipeModule_1.default { userContext ); } - return await this.config.errorHandlers.onUnauthorised(err.message, request, response); + return await this.config.errorHandlers.onUnauthorised(err.message, request, response, userContext); } else if (err.type === error_1.default.TRY_REFRESH_TOKEN) { logger_1.logDebugMessage("errorHandler: returning TRY_REFRESH_TOKEN"); - return await this.config.errorHandlers.onTryRefreshToken(err.message, request, response); + return await this.config.errorHandlers.onTryRefreshToken( + err.message, + request, + response, + userContext + ); } else if (err.type === error_1.default.TOKEN_THEFT_DETECTED) { logger_1.logDebugMessage("errorHandler: returning TOKEN_THEFT_DETECTED"); logger_1.logDebugMessage("errorHandler: Clearing tokens because of TOKEN_THEFT_DETECTED response"); @@ -128,15 +133,16 @@ class SessionRecipe extends recipeModule_1.default { err.payload.userId, err.payload.recipeUserId, request, - response + response, + userContext ); } else if (err.type === error_1.default.INVALID_CLAIMS) { - return await this.config.errorHandlers.onInvalidClaim(err.payload, request, response); + return await this.config.errorHandlers.onInvalidClaim(err.payload, request, response, userContext); } else { throw err; } } else { - return await this.openIdRecipe.handleError(err, request, response); + return await this.openIdRecipe.handleError(err, request, response, userContext); } }; this.getAllCORSHeaders = () => { diff --git a/lib/build/recipe/session/recipeImplementation.js b/lib/build/recipe/session/recipeImplementation.js index 11ae5f42f..fff1165f5 100644 --- a/lib/build/recipe/session/recipeImplementation.js +++ b/lib/build/recipe/session/recipeImplementation.js @@ -223,6 +223,7 @@ function getRecipeInterface(querier, config, appInfo, getRecipeImplAfterOverride input.userId, input.recipeUserId, accessTokenPayload.tId === undefined ? constants_1.DEFAULT_TENANT_ID : accessTokenPayload.tId, + accessTokenPayload, input.userContext ); logger_1.logDebugMessage( @@ -391,6 +392,7 @@ function getRecipeInterface(querier, config, appInfo, getRecipeImplAfterOverride sessionInfo.userId, sessionInfo.recipeUserId, sessionInfo.tenantId, + sessionInfo.customClaimsInAccessTokenPayload, input.userContext ); return this.mergeIntoAccessTokenPayload({ diff --git a/lib/build/recipe/session/sessionClass.d.ts b/lib/build/recipe/session/sessionClass.d.ts index 7553de3e0..2a1ea04f2 100644 --- a/lib/build/recipe/session/sessionClass.d.ts +++ b/lib/build/recipe/session/sessionClass.d.ts @@ -29,13 +29,13 @@ export default class Session implements SessionContainerInterface { accessTokenUpdated: boolean, tenantId: string ); - getRecipeUserId(_userContext?: any): RecipeUserId; - revokeSession(userContext?: any): Promise; - getSessionDataFromDatabase(userContext?: any): Promise; - updateSessionDataInDatabase(newSessionData: any, userContext?: any): Promise; - getUserId(_userContext?: any): string; - getTenantId(_userContext?: any): string; - getAccessTokenPayload(_userContext?: any): any; + getRecipeUserId(_userContext?: Record): RecipeUserId; + revokeSession(userContext?: Record): Promise; + getSessionDataFromDatabase(userContext?: Record): Promise; + updateSessionDataInDatabase(newSessionData: any, userContext?: Record): Promise; + getUserId(_userContext?: Record): string; + getTenantId(_userContext?: Record): string; + getAccessTokenPayload(_userContext?: Record): any; getHandle(): string; getAccessToken(): string; getAllSessionTokensDangerously(): { @@ -45,13 +45,13 @@ export default class Session implements SessionContainerInterface { frontToken: string; antiCsrfToken: string | undefined; }; - mergeIntoAccessTokenPayload(accessTokenPayloadUpdate: any, userContext?: any): Promise; - getTimeCreated(userContext?: any): Promise; - getExpiry(userContext?: any): Promise; - assertClaims(claimValidators: SessionClaimValidator[], userContext?: any): Promise; - fetchAndSetClaim(claim: SessionClaim, userContext?: any): Promise; - setClaimValue(claim: SessionClaim, value: T, userContext?: any): Promise; - getClaimValue(claim: SessionClaim, userContext?: any): Promise; - removeClaim(claim: SessionClaim, userContext?: any): Promise; - attachToRequestResponse(info: ReqResInfo, userContext?: any): void; + mergeIntoAccessTokenPayload(accessTokenPayloadUpdate: any, userContext?: Record): Promise; + getTimeCreated(userContext?: Record): Promise; + getExpiry(userContext?: Record): Promise; + assertClaims(claimValidators: SessionClaimValidator[], userContext?: Record): Promise; + fetchAndSetClaim(claim: SessionClaim, userContext?: Record): Promise; + setClaimValue(claim: SessionClaim, value: T, userContext?: Record): Promise; + getClaimValue(claim: SessionClaim, userContext?: Record): Promise; + removeClaim(claim: SessionClaim, userContext?: Record): Promise; + attachToRequestResponse(info: ReqResInfo, userContext?: Record): void; } diff --git a/lib/build/recipe/session/sessionClass.js b/lib/build/recipe/session/sessionClass.js index 8d6180ffb..7af4ba24b 100644 --- a/lib/build/recipe/session/sessionClass.js +++ b/lib/build/recipe/session/sessionClass.js @@ -58,9 +58,13 @@ class Session { return this.recipeUserId; } async revokeSession(userContext) { + const ctx = + userContext === undefined && this.reqResInfo !== undefined + ? utils_2.makeDefaultUserContextFromAPI(this.reqResInfo.req) + : utils_2.getUserContext(userContext); await this.helpers.getRecipeImpl().revokeSession({ sessionHandle: this.sessionHandle, - userContext: userContext === undefined ? {} : userContext, + userContext: ctx, }); if (this.reqResInfo !== undefined) { // we do not check the output of calling revokeSession @@ -74,14 +78,14 @@ class Session { this.reqResInfo.res, this.reqResInfo.transferMethod, this.reqResInfo.req, - userContext === undefined ? utils_2.makeDefaultUserContextFromAPI(this.reqResInfo.req) : userContext + ctx ); } } async getSessionDataFromDatabase(userContext) { let sessionInfo = await this.helpers.getRecipeImpl().getSessionInformation({ sessionHandle: this.sessionHandle, - userContext: userContext === undefined ? {} : userContext, + userContext: utils_2.getUserContext(userContext), }); if (sessionInfo === undefined) { logger_1.logDebugMessage( @@ -99,7 +103,7 @@ class Session { !(await this.helpers.getRecipeImpl().updateSessionDataInDatabase({ sessionHandle: this.sessionHandle, newSessionData, - userContext: userContext === undefined ? {} : userContext, + userContext: utils_2.getUserContext(userContext), })) ) { logger_1.logDebugMessage( @@ -138,7 +142,11 @@ class Session { } // Any update to this function should also be reflected in the respective JWT version async mergeIntoAccessTokenPayload(accessTokenPayloadUpdate, userContext) { - let newAccessTokenPayload = Object.assign({}, this.getAccessTokenPayload(userContext)); + const ctx = + userContext === undefined && this.reqResInfo !== undefined + ? utils_2.makeDefaultUserContextFromAPI(this.reqResInfo.req) + : utils_2.getUserContext(userContext); + let newAccessTokenPayload = Object.assign({}, this.getAccessTokenPayload(ctx)); for (const key of constants_1.protectedProps) { delete newAccessTokenPayload[key]; } @@ -151,7 +159,7 @@ class Session { let response = await this.helpers.getRecipeImpl().regenerateAccessToken({ accessToken: this.getAccessToken(), newAccessTokenPayload, - userContext: userContext === undefined ? {} : userContext, + userContext: ctx, }); if (response === undefined) { logger_1.logDebugMessage( @@ -178,7 +186,7 @@ class Session { this.helpers.config, this.reqResInfo.transferMethod, this.reqResInfo.req, - userContext === undefined ? utils_2.makeDefaultUserContextFromAPI(this.reqResInfo.req) : userContext + ctx ); } } else { @@ -186,7 +194,7 @@ class Session { // We can't update the access token on the FE, as it will need to call refresh anyway but we handle this as a successful update during this request. // the changes will be reflected on the FE after refresh is called this.userDataInAccessToken = Object.assign( - Object.assign({}, this.getAccessTokenPayload()), + Object.assign({}, this.getAccessTokenPayload(ctx)), response.session.userDataInJWT ); } @@ -194,7 +202,7 @@ class Session { async getTimeCreated(userContext) { let sessionInfo = await this.helpers.getRecipeImpl().getSessionInformation({ sessionHandle: this.sessionHandle, - userContext: userContext === undefined ? {} : userContext, + userContext: utils_2.getUserContext(userContext), }); if (sessionInfo === undefined) { logger_1.logDebugMessage("getTimeCreated: Throwing UNAUTHORISED because session does not exist anymore"); @@ -208,7 +216,7 @@ class Session { async getExpiry(userContext) { let sessionInfo = await this.helpers.getRecipeImpl().getSessionInformation({ sessionHandle: this.sessionHandle, - userContext: userContext === undefined ? {} : userContext, + userContext: utils_2.getUserContext(userContext), }); if (sessionInfo === undefined) { logger_1.logDebugMessage("getExpiry: Throwing UNAUTHORISED because session does not exist anymore"); @@ -221,18 +229,19 @@ class Session { } // Any update to this function should also be reflected in the respective JWT version async assertClaims(claimValidators, userContext) { + const ctx = utils_2.getUserContext(userContext); let validateClaimResponse = await this.helpers.getRecipeImpl().validateClaims({ - accessTokenPayload: this.getAccessTokenPayload(userContext), - userId: this.getUserId(userContext), - recipeUserId: this.getRecipeUserId(userContext), + accessTokenPayload: this.getAccessTokenPayload(ctx), + userId: this.getUserId(ctx), + recipeUserId: this.getRecipeUserId(ctx), claimValidators, - userContext, + userContext: ctx, }); if (validateClaimResponse.accessTokenPayloadUpdate !== undefined) { for (const key of constants_1.protectedProps) { delete validateClaimResponse.accessTokenPayloadUpdate[key]; } - await this.mergeIntoAccessTokenPayload(validateClaimResponse.accessTokenPayloadUpdate, userContext); + await this.mergeIntoAccessTokenPayload(validateClaimResponse.accessTokenPayloadUpdate, ctx); } if (validateClaimResponse.invalidClaims.length !== 0) { throw new error_1.default({ @@ -244,32 +253,41 @@ class Session { } // Any update to this function should also be reflected in the respective JWT version async fetchAndSetClaim(claim, userContext) { + const ctx = utils_2.getUserContext(userContext); const update = await claim.build( - this.getUserId(userContext), - this.getRecipeUserId(userContext), - this.getTenantId(), - userContext + this.getUserId(ctx), + this.getRecipeUserId(ctx), + this.getTenantId(ctx), + this.getAccessTokenPayload(ctx), + ctx ); - return this.mergeIntoAccessTokenPayload(update, userContext); + return this.mergeIntoAccessTokenPayload(update, ctx); } // Any update to this function should also be reflected in the respective JWT version setClaimValue(claim, value, userContext) { - const update = claim.addToPayload_internal({}, value, userContext); - return this.mergeIntoAccessTokenPayload(update, userContext); + const ctx = utils_2.getUserContext(userContext); + const update = claim.addToPayload_internal({}, value, utils_2.getUserContext(ctx)); + return this.mergeIntoAccessTokenPayload(update, ctx); } // Any update to this function should also be reflected in the respective JWT version async getClaimValue(claim, userContext) { - return claim.getValueFromPayload(await this.getAccessTokenPayload(userContext), userContext); + const ctx = utils_2.getUserContext(userContext); + return claim.getValueFromPayload(await this.getAccessTokenPayload(ctx), ctx); } // Any update to this function should also be reflected in the respective JWT version removeClaim(claim, userContext) { - const update = claim.removeFromPayloadByMerge_internal({}, userContext); - return this.mergeIntoAccessTokenPayload(update, userContext); + const ctx = utils_2.getUserContext(userContext); + const update = claim.removeFromPayloadByMerge_internal({}, ctx); + return this.mergeIntoAccessTokenPayload(update, ctx); } attachToRequestResponse(info, userContext) { this.reqResInfo = info; if (this.accessTokenUpdated) { const { res, transferMethod } = info; + const ctx = + userContext !== undefined + ? utils_2.getUserContext(userContext) + : utils_2.makeDefaultUserContextFromAPI(info.req); utils_1.setAccessTokenInResponse( res, this.accessToken, @@ -277,7 +295,7 @@ class Session { this.helpers.config, transferMethod, info.req, - userContext !== undefined ? userContext : utils_2.makeDefaultUserContextFromAPI(info.req) + ctx ); if (this.refreshToken !== undefined) { cookieAndHeaders_1.setToken( @@ -288,7 +306,7 @@ class Session { this.refreshToken.expiry, transferMethod, info.req, - userContext !== undefined ? userContext : utils_2.makeDefaultUserContextFromAPI(info.req) + ctx ); } if (this.antiCsrfToken !== undefined) { diff --git a/lib/build/recipe/session/sessionFunctions.d.ts b/lib/build/recipe/session/sessionFunctions.d.ts index 39e156bf6..f52334590 100644 --- a/lib/build/recipe/session/sessionFunctions.d.ts +++ b/lib/build/recipe/session/sessionFunctions.d.ts @@ -3,6 +3,7 @@ import { ParsedJWTInfo } from "./jwt"; import { CreateOrRefreshAPIResponse, SessionInformation } from "./types"; import { Helpers } from "./recipeImplementation"; import RecipeUserId from "../../recipeUserId"; +import { UserContext } from "../../types"; /** * @description call this to "login" a user. */ @@ -13,7 +14,7 @@ export declare function createNewSession( disableAntiCsrf: boolean, accessTokenPayload: any, sessionDataInDatabase: any, - userContext: any + userContext: UserContext ): Promise; /** * @description authenticates a session. To be used in APIs that require authentication @@ -24,7 +25,7 @@ export declare function getSession( antiCsrfToken: string | undefined, doAntiCsrfCheck: boolean, alwaysCheckCore: boolean, - userContext: any + userContext: UserContext ): Promise<{ session: { handle: string; @@ -47,7 +48,7 @@ export declare function getSession( export declare function getSessionInformation( helpers: Helpers, sessionHandle: string, - userContext: any + userContext: UserContext ): Promise; /** * @description generates new access and refresh tokens for a given refresh token. Called when client's access token has expired. @@ -58,7 +59,7 @@ export declare function refreshSession( refreshToken: string, antiCsrfToken: string | undefined, disableAntiCsrf: boolean, - userContext: any + userContext: UserContext ): Promise; /** * @description deletes session info of a user from db. This only invalidates the refresh token. Not the access token. @@ -70,7 +71,7 @@ export declare function revokeAllSessionsForUser( revokeSessionsForLinkedAccounts: boolean, tenantId: string | undefined, revokeAcrossAllTenants: boolean | undefined, - userContext: any + userContext: UserContext ): Promise; /** * @description gets all session handles for current user. Please do not call this unless this user is authenticated. @@ -81,13 +82,17 @@ export declare function getAllSessionHandlesForUser( fetchSessionsForAllLinkedAccounts: boolean, tenantId: string | undefined, fetchAcrossAllTenants: boolean | undefined, - userContext: any + userContext: UserContext ): Promise; /** * @description call to destroy one session * @returns true if session was deleted from db. Else false in case there was nothing to delete */ -export declare function revokeSession(helpers: Helpers, sessionHandle: string, userContext: any): Promise; +export declare function revokeSession( + helpers: Helpers, + sessionHandle: string, + userContext: UserContext +): Promise; /** * @description call to destroy multiple sessions * @returns list of sessions revoked @@ -95,7 +100,7 @@ export declare function revokeSession(helpers: Helpers, sessionHandle: string, u export declare function revokeMultipleSessions( helpers: Helpers, sessionHandles: string[], - userContext: any + userContext: UserContext ): Promise; /** * @description: It provides no locking mechanism in case other processes are updating session data for this session as well. @@ -104,11 +109,11 @@ export declare function updateSessionDataInDatabase( helpers: Helpers, sessionHandle: string, newSessionData: any, - userContext: any + userContext: UserContext ): Promise; export declare function updateAccessTokenPayload( helpers: Helpers, sessionHandle: string, newAccessTokenPayload: any, - userContext: any + userContext: UserContext ): Promise; diff --git a/lib/build/recipe/session/sessionFunctions.js b/lib/build/recipe/session/sessionFunctions.js index 9f7a8d0c3..b73c1aead 100644 --- a/lib/build/recipe/session/sessionFunctions.js +++ b/lib/build/recipe/session/sessionFunctions.js @@ -37,8 +37,8 @@ async function createNewSession( tenantId, recipeUserId, disableAntiCsrf, - accessTokenPayload, - sessionDataInDatabase, + accessTokenPayload = {}, + sessionDataInDatabase = {}, userContext ) { accessTokenPayload = accessTokenPayload === null || accessTokenPayload === undefined ? {} : accessTokenPayload; diff --git a/lib/build/recipe/session/sessionRequestFunctions.d.ts b/lib/build/recipe/session/sessionRequestFunctions.d.ts index f94e2dff4..940143888 100644 --- a/lib/build/recipe/session/sessionRequestFunctions.d.ts +++ b/lib/build/recipe/session/sessionRequestFunctions.d.ts @@ -1,7 +1,7 @@ // @ts-nocheck import Recipe from "./recipe"; import { VerifySessionOptions, RecipeInterface, TypeNormalisedInput, SessionContainerInterface } from "./types"; -import { NormalisedAppinfo } from "../../types"; +import { NormalisedAppinfo, UserContext } from "../../types"; import RecipeUserId from "../../recipeUserId"; export declare function getSessionFromRequest({ req, @@ -16,7 +16,7 @@ export declare function getSessionFromRequest({ config: TypeNormalisedInput; recipeInterfaceImpl: RecipeInterface; options?: VerifySessionOptions; - userContext?: any; + userContext?: UserContext; }): Promise; export declare function refreshSessionInRequest({ res, @@ -27,7 +27,7 @@ export declare function refreshSessionInRequest({ }: { res: any; req: any; - userContext: any; + userContext: UserContext; config: TypeNormalisedInput; recipeInterfaceImpl: RecipeInterface; }): Promise; @@ -46,7 +46,7 @@ export declare function createNewSessionInRequest({ }: { req: any; res: any; - userContext: any; + userContext: UserContext; recipeInstance: Recipe; accessTokenPayload: any; userId: string; diff --git a/lib/build/recipe/session/sessionRequestFunctions.js b/lib/build/recipe/session/sessionRequestFunctions.js index fee9b14bc..252b96b90 100644 --- a/lib/build/recipe/session/sessionRequestFunctions.js +++ b/lib/build/recipe/session/sessionRequestFunctions.js @@ -348,7 +348,7 @@ async function createNewSessionInRequest({ delete finalAccessTokenPayload[prop]; } for (const claim of claimsAddedByOtherRecipes) { - const update = await claim.build(userId, recipeUserId, tenantId, userContext); + const update = await claim.build(userId, recipeUserId, tenantId, finalAccessTokenPayload, userContext); finalAccessTokenPayload = Object.assign(Object.assign({}, finalAccessTokenPayload), update); } logger_1.logDebugMessage("createNewSession: Access token payload built"); diff --git a/lib/build/recipe/session/types.d.ts b/lib/build/recipe/session/types.d.ts index 220ba2f20..fb45daf31 100644 --- a/lib/build/recipe/session/types.d.ts +++ b/lib/build/recipe/session/types.d.ts @@ -4,7 +4,7 @@ import NormalisedURLPath from "../../normalisedURLPath"; import { RecipeInterface as JWTRecipeInterface, APIInterface as JWTAPIInterface } from "../jwt/types"; import OverrideableBuilder from "supertokens-js-override"; import { RecipeInterface as OpenIdRecipeInterface, APIInterface as OpenIdAPIInterface } from "../openid/types"; -import { JSONObject, JSONValue } from "../../types"; +import { JSONObject, JSONValue, UserContext } from "../../types"; import { GeneralErrorResponse } from "../../types"; import RecipeUserId from "../../recipeUserId"; export declare type KeyInfo = { @@ -45,10 +45,11 @@ export declare type TypeInput = { cookieSecure?: boolean; cookieSameSite?: "strict" | "lax" | "none"; cookieDomain?: string; + overwriteSessionDuringSignIn?: boolean; getTokenTransferMethod?: (input: { req: BaseRequest; forCreateNewSession: boolean; - userContext: any; + userContext: UserContext; }) => TokenTransferMethod | "any"; errorHandlers?: ErrorHandlers; antiCsrf?: "VIA_TOKEN" | "VIA_CUSTOM_HEADER" | "NONE"; @@ -86,19 +87,23 @@ export declare type TypeNormalisedInput = { refreshTokenPath: NormalisedURLPath; accessTokenPath: NormalisedURLPath; cookieDomain: string | undefined; - getCookieSameSite: (input: { request: BaseRequest | undefined; userContext: any }) => "strict" | "lax" | "none"; + getCookieSameSite: (input: { + request: BaseRequest | undefined; + userContext: UserContext; + }) => "strict" | "lax" | "none"; cookieSecure: boolean; sessionExpiredStatusCode: number; errorHandlers: NormalisedErrorHandlers; + overwriteSessionDuringSignIn: boolean; antiCsrfFunctionOrString: | "VIA_TOKEN" | "VIA_CUSTOM_HEADER" | "NONE" - | ((input: { request: BaseRequest | undefined; userContext: any }) => "VIA_CUSTOM_HEADER" | "NONE"); + | ((input: { request: BaseRequest | undefined; userContext: UserContext }) => "VIA_CUSTOM_HEADER" | "NONE"); getTokenTransferMethod: (input: { req: BaseRequest; forCreateNewSession: boolean; - userContext: any; + userContext: UserContext; }) => TokenTransferMethod | "any"; invalidClaimStatusCode: number; exposeAccessTokenToFrontendInCookieBasedAuth: boolean; @@ -134,7 +139,7 @@ export interface SessionRequest extends BaseRequest { session?: SessionContainerInterface; } export interface ErrorHandlerMiddleware { - (message: string, request: BaseRequest, response: BaseResponse): Promise; + (message: string, request: BaseRequest, response: BaseResponse, userContext: UserContext): Promise; } export interface TokenTheftErrorHandlerMiddleware { ( @@ -142,11 +147,17 @@ export interface TokenTheftErrorHandlerMiddleware { userId: string, recipeUserId: RecipeUserId, request: BaseRequest, - response: BaseResponse + response: BaseResponse, + userContext: UserContext ): Promise; } export interface InvalidClaimErrorHandlerMiddleware { - (validatorErrors: ClaimValidationError[], request: BaseRequest, response: BaseResponse): Promise; + ( + validatorErrors: ClaimValidationError[], + request: BaseRequest, + response: BaseResponse, + userContext: UserContext + ): Promise; } export interface NormalisedErrorHandlers { onUnauthorised: ErrorHandlerMiddleware; @@ -161,7 +172,7 @@ export interface VerifySessionOptions { overrideGlobalClaimValidators?: ( globalClaimValidators: SessionClaimValidator[], session: SessionContainerInterface, - userContext: any + userContext: UserContext ) => Promise | SessionClaimValidator[]; } export declare type RecipeInterface = { @@ -172,26 +183,26 @@ export declare type RecipeInterface = { sessionDataInDatabase?: any; disableAntiCsrf?: boolean; tenantId: string; - userContext: any; + userContext: UserContext; }): Promise; getGlobalClaimValidators(input: { tenantId: string; userId: string; recipeUserId: RecipeUserId; claimValidatorsAddedByOtherRecipes: SessionClaimValidator[]; - userContext: any; + userContext: UserContext; }): Promise | SessionClaimValidator[]; getSession(input: { accessToken: string | undefined; antiCsrfToken?: string; options?: VerifySessionOptions; - userContext: any; + userContext: UserContext; }): Promise; refreshSession(input: { refreshToken: string; antiCsrfToken?: string; disableAntiCsrf: boolean; - userContext: any; + userContext: UserContext; }): Promise; /** * Used to retrieve all session information for a given session handle. Can be used in place of: @@ -200,32 +211,35 @@ export declare type RecipeInterface = { * * Returns undefined if the sessionHandle does not exist */ - getSessionInformation(input: { sessionHandle: string; userContext: any }): Promise; + getSessionInformation(input: { + sessionHandle: string; + userContext: UserContext; + }): Promise; revokeAllSessionsForUser(input: { userId: string; revokeSessionsForLinkedAccounts: boolean; tenantId: string; revokeAcrossAllTenants?: boolean; - userContext: any; + userContext: UserContext; }): Promise; getAllSessionHandlesForUser(input: { userId: string; fetchSessionsForAllLinkedAccounts: boolean; tenantId: string; fetchAcrossAllTenants?: boolean; - userContext: any; + userContext: UserContext; }): Promise; - revokeSession(input: { sessionHandle: string; userContext: any }): Promise; - revokeMultipleSessions(input: { sessionHandles: string[]; userContext: any }): Promise; + revokeSession(input: { sessionHandle: string; userContext: UserContext }): Promise; + revokeMultipleSessions(input: { sessionHandles: string[]; userContext: UserContext }): Promise; updateSessionDataInDatabase(input: { sessionHandle: string; newSessionData: any; - userContext: any; + userContext: UserContext; }): Promise; mergeIntoAccessTokenPayload(input: { sessionHandle: string; accessTokenPayloadUpdate: JSONObject; - userContext: any; + userContext: UserContext; }): Promise; /** * @returns {Promise} Returns false if the sessionHandle does not exist @@ -233,7 +247,7 @@ export declare type RecipeInterface = { regenerateAccessToken(input: { accessToken: string; newAccessTokenPayload?: any; - userContext: any; + userContext: UserContext; }): Promise< | { status: "OK"; @@ -257,22 +271,26 @@ export declare type RecipeInterface = { recipeUserId: RecipeUserId; accessTokenPayload: any; claimValidators: SessionClaimValidator[]; - userContext: any; + userContext: UserContext; }): Promise<{ invalidClaims: ClaimValidationError[]; accessTokenPayloadUpdate?: any; }>; - fetchAndSetClaim(input: { sessionHandle: string; claim: SessionClaim; userContext: any }): Promise; + fetchAndSetClaim(input: { + sessionHandle: string; + claim: SessionClaim; + userContext: UserContext; + }): Promise; setClaimValue(input: { sessionHandle: string; claim: SessionClaim; value: T; - userContext: any; + userContext: UserContext; }): Promise; getClaimValue(input: { sessionHandle: string; claim: SessionClaim; - userContext: any; + userContext: UserContext; }): Promise< | { status: "SESSION_DOES_NOT_EXIST_ERROR"; @@ -282,17 +300,17 @@ export declare type RecipeInterface = { value: T | undefined; } >; - removeClaim(input: { sessionHandle: string; claim: SessionClaim; userContext: any }): Promise; + removeClaim(input: { sessionHandle: string; claim: SessionClaim; userContext: UserContext }): Promise; }; export interface SessionContainerInterface { - revokeSession(userContext?: any): Promise; - getSessionDataFromDatabase(userContext?: any): Promise; - updateSessionDataInDatabase(newSessionData: any, userContext?: any): Promise; - getUserId(userContext?: any): string; - getRecipeUserId(userContext?: any): RecipeUserId; - getTenantId(userContext?: any): string; - getAccessTokenPayload(userContext?: any): any; - getHandle(userContext?: any): string; + revokeSession(userContext?: UserContext): Promise; + getSessionDataFromDatabase(userContext?: UserContext): Promise; + updateSessionDataInDatabase(newSessionData: any, userContext?: UserContext): Promise; + getUserId(userContext?: UserContext): string; + getRecipeUserId(userContext?: UserContext): RecipeUserId; + getTenantId(userContext?: UserContext): string; + getAccessTokenPayload(userContext?: UserContext): any; + getHandle(userContext?: UserContext): string; getAllSessionTokensDangerously(): { accessToken: string; refreshToken: string | undefined; @@ -300,16 +318,16 @@ export interface SessionContainerInterface { frontToken: string; accessAndFrontTokenUpdated: boolean; }; - getAccessToken(userContext?: any): string; - mergeIntoAccessTokenPayload(accessTokenPayloadUpdate: JSONObject, userContext?: any): Promise; - getTimeCreated(userContext?: any): Promise; - getExpiry(userContext?: any): Promise; - assertClaims(claimValidators: SessionClaimValidator[], userContext?: any): Promise; - fetchAndSetClaim(claim: SessionClaim, userContext?: any): Promise; - setClaimValue(claim: SessionClaim, value: T, userContext?: any): Promise; - getClaimValue(claim: SessionClaim, userContext?: any): Promise; - removeClaim(claim: SessionClaim, userContext?: any): Promise; - attachToRequestResponse(reqResInfo: ReqResInfo, userContext?: any): Promise | void; + getAccessToken(userContext?: UserContext): string; + mergeIntoAccessTokenPayload(accessTokenPayloadUpdate: JSONObject, userContext?: UserContext): Promise; + getTimeCreated(userContext?: UserContext): Promise; + getExpiry(userContext?: UserContext): Promise; + assertClaims(claimValidators: SessionClaimValidator[], userContext?: UserContext): Promise; + fetchAndSetClaim(claim: SessionClaim, userContext?: UserContext): Promise; + setClaimValue(claim: SessionClaim, value: T, userContext?: UserContext): Promise; + getClaimValue(claim: SessionClaim, userContext?: UserContext): Promise; + removeClaim(claim: SessionClaim, userContext?: UserContext): Promise; + attachToRequestResponse(reqResInfo: ReqResInfo, userContext?: UserContext): Promise | void; } export declare type APIOptions = { recipeImplementation: RecipeInterface; @@ -325,13 +343,15 @@ export declare type APIInterface = { * since it's not something that is directly called by the user on the * frontend anyway */ - refreshPOST: undefined | ((input: { options: APIOptions; userContext: any }) => Promise); + refreshPOST: + | undefined + | ((input: { options: APIOptions; userContext: UserContext }) => Promise); signOutPOST: | undefined | ((input: { options: APIOptions; session: SessionContainerInterface | undefined; - userContext: any; + userContext: UserContext; }) => Promise< | { status: "OK"; @@ -341,7 +361,7 @@ export declare type APIInterface = { verifySession(input: { verifySessionOptions: VerifySessionOptions | undefined; options: APIOptions; - userContext: any; + userContext: UserContext; }): Promise; }; export declare type SessionInformation = { @@ -374,7 +394,7 @@ export declare type SessionClaimValidator = ( * Decides if we need to refetch the claim value before checking the payload with `isValid`. * E.g.: if the information in the payload is expired, or is not sufficient for this check. */ - shouldRefetch: (payload: any, userContext: any) => Promise | boolean; + shouldRefetch: (payload: any, userContext: UserContext) => Promise | boolean; } | {} ) & { @@ -382,7 +402,7 @@ export declare type SessionClaimValidator = ( /** * Decides if the claim is valid based on the payload (and not checking DB or anything else) */ - validate: (payload: any, userContext: any) => Promise; + validate: (payload: any, userContext: UserContext) => Promise; }; export declare abstract class SessionClaim { readonly key: string; @@ -396,33 +416,40 @@ export declare abstract class SessionClaim { userId: string, recipeUserId: RecipeUserId, tenantId: string, - userContext: any + currentPayload: JSONObject | undefined, + userContext: UserContext ): Promise | T | undefined; /** * Saves the provided value into the payload, by cloning and updating the entire object. * * @returns The modified payload object */ - abstract addToPayload_internal(payload: JSONObject, value: T, userContext: any): JSONObject; + abstract addToPayload_internal(payload: JSONObject, value: T, userContext: UserContext): JSONObject; /** * Removes the claim from the payload by setting it to null, so mergeIntoAccessTokenPayload clears it * * @returns The modified payload object */ - abstract removeFromPayloadByMerge_internal(payload: JSONObject, userContext?: any): JSONObject; + abstract removeFromPayloadByMerge_internal(payload: JSONObject, userContext?: UserContext): JSONObject; /** * Removes the claim from the payload, by cloning and updating the entire object. * * @returns The modified payload object */ - abstract removeFromPayload(payload: JSONObject, userContext?: any): JSONObject; + abstract removeFromPayload(payload: JSONObject, userContext?: UserContext): JSONObject; /** * Gets the value of the claim stored in the payload * * @returns Claim value */ - abstract getValueFromPayload(payload: JSONObject, userContext: any): T | undefined; - build(userId: string, recipeUserId: RecipeUserId, tenantId: string, userContext?: any): Promise; + abstract getValueFromPayload(payload: JSONObject, userContext: UserContext): T | undefined; + build( + userId: string, + recipeUserId: RecipeUserId, + tenantId: string, + currentPayload: JSONObject | undefined, + userContext: UserContext + ): Promise; } export declare type ReqResInfo = { res: BaseResponse; diff --git a/lib/build/recipe/session/types.js b/lib/build/recipe/session/types.js index 28aa0c726..9081334a0 100644 --- a/lib/build/recipe/session/types.js +++ b/lib/build/recipe/session/types.js @@ -5,8 +5,8 @@ class SessionClaim { constructor(key) { this.key = key; } - async build(userId, recipeUserId, tenantId, userContext) { - const value = await this.fetchValue(userId, recipeUserId, tenantId, userContext); + async build(userId, recipeUserId, tenantId, currentPayload, userContext) { + const value = await this.fetchValue(userId, recipeUserId, tenantId, currentPayload, userContext); if (value === undefined) { return {}; } diff --git a/lib/build/recipe/session/utils.d.ts b/lib/build/recipe/session/utils.d.ts index 6459666f0..24ce87cb5 100644 --- a/lib/build/recipe/session/utils.d.ts +++ b/lib/build/recipe/session/utils.d.ts @@ -9,26 +9,29 @@ import { TokenTransferMethod, } from "./types"; import SessionRecipe from "./recipe"; -import { NormalisedAppinfo } from "../../types"; +import { NormalisedAppinfo, UserContext } from "../../types"; import type { BaseRequest, BaseResponse } from "../../framework"; import RecipeUserId from "../../recipeUserId"; export declare function sendTryRefreshTokenResponse( recipeInstance: SessionRecipe, _: string, __: BaseRequest, - response: BaseResponse + response: BaseResponse, + ___: UserContext ): Promise; export declare function sendUnauthorisedResponse( recipeInstance: SessionRecipe, _: string, __: BaseRequest, - response: BaseResponse + response: BaseResponse, + ___: UserContext ): Promise; export declare function sendInvalidClaimResponse( recipeInstance: SessionRecipe, claimValidationErrors: ClaimValidationError[], __: BaseRequest, - response: BaseResponse + response: BaseResponse, + ___: UserContext ): Promise; export declare function sendTokenTheftDetectedResponse( recipeInstance: SessionRecipe, @@ -36,7 +39,8 @@ export declare function sendTokenTheftDetectedResponse( _: string, __: RecipeUserId, ___: BaseRequest, - response: BaseResponse + response: BaseResponse, + userContext: UserContext ): Promise; export declare function normaliseSessionScopeOrThrowError(sessionScope: string): string; export declare function getURLProtocol(url: string): string; @@ -53,17 +57,17 @@ export declare function setAccessTokenInResponse( config: TypeNormalisedInput, transferMethod: TokenTransferMethod, req: BaseRequest | undefined, - userContext: any + userContext: UserContext ): void; export declare function getRequiredClaimValidators( session: SessionContainerInterface, overrideGlobalClaimValidators: VerifySessionOptions["overrideGlobalClaimValidators"], - userContext: any + userContext: UserContext ): Promise; export declare function validateClaimsInPayload( claimValidators: SessionClaimValidator[], newAccessTokenPayload: any, - userContext: any + userContext: UserContext ): Promise< { id: string; diff --git a/lib/build/recipe/session/utils.js b/lib/build/recipe/session/utils.js index 6657eb33f..02cb15fac 100644 --- a/lib/build/recipe/session/utils.js +++ b/lib/build/recipe/session/utils.js @@ -27,7 +27,7 @@ const normalisedURLPath_1 = __importDefault(require("../../normalisedURLPath")); const utils_1 = require("../../utils"); const utils_2 = require("../../utils"); const logger_1 = require("../../logger"); -async function sendTryRefreshTokenResponse(recipeInstance, _, __, response) { +async function sendTryRefreshTokenResponse(recipeInstance, _, __, response, ___) { utils_2.sendNon200ResponseWithMessage( response, "try refresh token", @@ -35,19 +35,19 @@ async function sendTryRefreshTokenResponse(recipeInstance, _, __, response) { ); } exports.sendTryRefreshTokenResponse = sendTryRefreshTokenResponse; -async function sendUnauthorisedResponse(recipeInstance, _, __, response) { +async function sendUnauthorisedResponse(recipeInstance, _, __, response, ___) { utils_2.sendNon200ResponseWithMessage(response, "unauthorised", recipeInstance.config.sessionExpiredStatusCode); } exports.sendUnauthorisedResponse = sendUnauthorisedResponse; -async function sendInvalidClaimResponse(recipeInstance, claimValidationErrors, __, response) { +async function sendInvalidClaimResponse(recipeInstance, claimValidationErrors, __, response, ___) { utils_2.sendNon200Response(response, recipeInstance.config.invalidClaimStatusCode, { message: "invalid claim", claimValidationErrors, }); } exports.sendInvalidClaimResponse = sendInvalidClaimResponse; -async function sendTokenTheftDetectedResponse(recipeInstance, sessionHandle, _, __, ___, response) { - await recipeInstance.recipeInterfaceImpl.revokeSession({ sessionHandle, userContext: {} }); +async function sendTokenTheftDetectedResponse(recipeInstance, sessionHandle, _, __, ___, response, userContext) { + await recipeInstance.recipeInterfaceImpl.revokeSession({ sessionHandle, userContext }); utils_2.sendNon200ResponseWithMessage( response, "token theft detected", @@ -93,7 +93,7 @@ function getURLProtocol(url) { } exports.getURLProtocol = getURLProtocol; function validateAndNormaliseUserInput(recipeInstance, appInfo, config) { - var _a, _b, _c; + var _a, _b, _c, _d; let cookieDomain = config === undefined || config.cookieDomain === undefined ? undefined @@ -153,24 +153,25 @@ function validateAndNormaliseUserInput(recipeInstance, appInfo, config) { antiCsrf = config.antiCsrf; } let errorHandlers = { - onTokenTheftDetected: async (sessionHandle, userId, recipeUserId, request, response) => { + onTokenTheftDetected: async (sessionHandle, userId, recipeUserId, request, response, userContext) => { return await sendTokenTheftDetectedResponse( recipeInstance, sessionHandle, userId, recipeUserId, request, - response + response, + userContext ); }, - onTryRefreshToken: async (message, request, response) => { - return await sendTryRefreshTokenResponse(recipeInstance, message, request, response); + onTryRefreshToken: async (message, request, response, userContext) => { + return await sendTryRefreshTokenResponse(recipeInstance, message, request, response, userContext); }, - onUnauthorised: async (message, request, response) => { - return await sendUnauthorisedResponse(recipeInstance, message, request, response); + onUnauthorised: async (message, request, response, userContext) => { + return await sendUnauthorisedResponse(recipeInstance, message, request, response, userContext); }, - onInvalidClaim: (validationErrors, request, response) => { - return sendInvalidClaimResponse(recipeInstance, validationErrors, request, response); + onInvalidClaim: (validationErrors, request, response, userContext) => { + return sendInvalidClaimResponse(recipeInstance, validationErrors, request, response, userContext); }, }; if (config !== undefined && config.errorHandlers !== undefined) { @@ -217,6 +218,11 @@ function validateAndNormaliseUserInput(recipeInstance, appInfo, config) { antiCsrfFunctionOrString: antiCsrf, override, invalidClaimStatusCode, + overwriteSessionDuringSignIn: + (_d = config === null || config === void 0 ? void 0 : config.overwriteSessionDuringSignIn) !== null && + _d !== void 0 + ? _d + : false, }; } exports.validateAndNormaliseUserInput = validateAndNormaliseUserInput; diff --git a/lib/build/recipe/thirdparty/api/appleRedirect.d.ts b/lib/build/recipe/thirdparty/api/appleRedirect.d.ts index 2a4ac921f..ae0e54770 100644 --- a/lib/build/recipe/thirdparty/api/appleRedirect.d.ts +++ b/lib/build/recipe/thirdparty/api/appleRedirect.d.ts @@ -1,7 +1,8 @@ // @ts-nocheck import { APIInterface, APIOptions } from "../"; +import { UserContext } from "../../../types"; export default function appleRedirectHandler( apiImplementation: APIInterface, options: APIOptions, - userContext: any + userContext: UserContext ): Promise; diff --git a/lib/build/recipe/thirdparty/api/authorisationUrl.d.ts b/lib/build/recipe/thirdparty/api/authorisationUrl.d.ts index 9679fd6fd..dac53edea 100644 --- a/lib/build/recipe/thirdparty/api/authorisationUrl.d.ts +++ b/lib/build/recipe/thirdparty/api/authorisationUrl.d.ts @@ -1,8 +1,9 @@ // @ts-nocheck import { APIInterface, APIOptions } from "../"; +import { UserContext } from "../../../types"; export default function authorisationUrlAPI( apiImplementation: APIInterface, tenantId: string, options: APIOptions, - userContext: any + userContext: UserContext ): Promise; diff --git a/lib/build/recipe/thirdparty/api/implementation.js b/lib/build/recipe/thirdparty/api/implementation.js index ade405ca3..94d1c0009 100644 --- a/lib/build/recipe/thirdparty/api/implementation.js +++ b/lib/build/recipe/thirdparty/api/implementation.js @@ -10,6 +10,8 @@ const recipe_1 = __importDefault(require("../../accountlinking/recipe")); const __1 = require("../../.."); const emailverification_1 = __importDefault(require("../../emailverification")); const recipe_2 = __importDefault(require("../../emailverification/recipe")); +const recipe_3 = __importDefault(require("../../multifactorauth/recipe")); +const error_1 = __importDefault(require("../../session/error")); function getAPIInterface() { return { authorisationUrlGET: async function ({ provider, redirectURIOnProviderDashboard, userContext }) { @@ -182,6 +184,48 @@ function getAPIInterface() { }; } } + const userLoggingIn = existingUsers[0]; + const mfaInstance = recipe_3.default.getInstance(); + let session = await session_1.default.getSession(input.options.req, input.options.res, { + sessionRequired: false, + overrideGlobalClaimValidators: () => [], + }); + let sessionUser; + if (session !== undefined) { + if (userLoggingIn && userLoggingIn.id === session.getUserId()) { + sessionUser = userLoggingIn; + } else { + const user = await __1.getUser(session.getUserId(), input.userContext); + if (user === undefined) { + throw new error_1.default({ + type: error_1.default.UNAUTHORISED, + message: "Session user not found", + }); + } + sessionUser = user; + } + } + let isAlreadySetup = undefined; + if (mfaInstance) { + isAlreadySetup = !sessionUser ? false : sessionUser.thirdParty.length > 0; + const validateMfaRes = await mfaInstance.validateForMultifactorAuthBeforeFactorCompletion({ + req: input.options.req, + res: input.options.res, + tenantId: input.tenantId, + factorIdInProgress: "thirdparty", + session, + userLoggingIn, + isAlreadySetup, + signUpInfo: { + email: emailInfo.id, + isVerifiedFactor: emailInfo.isVerified, + }, + userContext: input.userContext, + }); + if (validateMfaRes.status !== "OK") { + return validateMfaRes; + } + } let response = await options.recipeImplementation.signInUp({ thirdPartyId: provider.id, thirdPartyUserId: userInfo.thirdPartyUserId, @@ -190,6 +234,8 @@ function getAPIInterface() { oAuthTokens: oAuthTokensToUse, rawUserInfoFromProvider: userInfo.rawUserInfoFromProvider, tenantId, + // we do not want to attempt accountlinking when there is an active session and MFA is turned on + shouldAttemptAccountLinkingIfAllowed: session === undefined || mfaInstance === undefined, userContext, }); if (response.status === "SIGN_IN_UP_NOT_ALLOWED") { @@ -233,26 +279,56 @@ function getAPIInterface() { } // we do account linking only during sign in here cause during sign up, // the recipe function above does account linking for us. - response.user = await recipe_1.default.getInstance().createPrimaryUserIdOrLinkAccounts({ + // we do not want to attempt accountlinking when there is an active session and MFA is turned on + if (session === undefined || mfaInstance === undefined) { + response.user = await recipe_1.default.getInstance().createPrimaryUserIdOrLinkAccounts({ + tenantId, + user: response.user, + userContext, + }); + } + } + if (mfaInstance === undefined) { + // No MFA, create session as usual + let session = await session_1.default.createNewOrKeepExistingSession( + options.req, + options.res, tenantId, + loginMethod.recipeUserId, + {}, + {}, + userContext + ); + return { + status: "OK", + createdNewRecipeUser: response.createdNewRecipeUser, user: response.user, - userContext, - }); + session, + oAuthTokens: oAuthTokensToUse, + rawUserInfoFromProvider: userInfo.rawUserInfoFromProvider, + }; } - let session = await session_1.default.createNewSession( - options.req, - options.res, + const sessionRes = await mfaInstance.createOrUpdateSessionForMultifactorAuthAfterFactorCompletion({ + req: options.req, + res: options.res, tenantId, - loginMethod.recipeUserId, - {}, - {}, - userContext - ); + factorIdInProgress: "thirdparty", + isAlreadySetup, + justCompletedFactorUserInfo: { + user: response.user, + createdNewUser: response.createdNewRecipeUser, + recipeUserId: loginMethod.recipeUserId, + }, + userContext: input.userContext, + }); + if (sessionRes.status !== "OK") { + return sessionRes; + } return { status: "OK", createdNewRecipeUser: response.createdNewRecipeUser, - user: response.user, - session, + user: await __1.getUser(response.user.id, userContext), + session: sessionRes.session, oAuthTokens: oAuthTokensToUse, rawUserInfoFromProvider: userInfo.rawUserInfoFromProvider, }; diff --git a/lib/build/recipe/thirdparty/api/signinup.d.ts b/lib/build/recipe/thirdparty/api/signinup.d.ts index 53d005ee6..06f84bb63 100644 --- a/lib/build/recipe/thirdparty/api/signinup.d.ts +++ b/lib/build/recipe/thirdparty/api/signinup.d.ts @@ -1,8 +1,9 @@ // @ts-nocheck import { APIInterface, APIOptions } from "../"; +import { UserContext } from "../../../types"; export default function signInUpAPI( apiImplementation: APIInterface, tenantId: string, options: APIOptions, - userContext: any + userContext: UserContext ): Promise; diff --git a/lib/build/recipe/thirdparty/index.d.ts b/lib/build/recipe/thirdparty/index.d.ts index 85c6d93ac..04cb35dd5 100644 --- a/lib/build/recipe/thirdparty/index.d.ts +++ b/lib/build/recipe/thirdparty/index.d.ts @@ -9,7 +9,7 @@ export default class Wrapper { tenantId: string, thirdPartyId: string, clientType: string | undefined, - userContext?: any + userContext?: Record ): Promise; static manuallyCreateOrUpdateUser( tenantId: string, @@ -17,14 +17,14 @@ export default class Wrapper { thirdPartyUserId: string, email: string, isVerified: boolean, - userContext?: any + shouldAttemptAccountLinkingIfAllowed?: boolean, + userContext?: Record ): Promise< | { status: "OK"; createdNewRecipeUser: boolean; user: import("../../types").User; recipeUserId: import("../..").RecipeUserId; - isValidFirstFactorForTenant: boolean | undefined; } | { status: "EMAIL_CHANGE_NOT_ALLOWED_ERROR"; diff --git a/lib/build/recipe/thirdparty/index.js b/lib/build/recipe/thirdparty/index.js index 941228742..0e0058122 100644 --- a/lib/build/recipe/thirdparty/index.js +++ b/lib/build/recipe/thirdparty/index.js @@ -23,13 +23,14 @@ exports.manuallyCreateOrUpdateUser = exports.getProvider = exports.Error = expor const recipe_1 = __importDefault(require("./recipe")); const error_1 = __importDefault(require("./error")); const constants_1 = require("../multitenancy/constants"); +const utils_1 = require("../../utils"); class Wrapper { - static async getProvider(tenantId, thirdPartyId, clientType, userContext = {}) { + static async getProvider(tenantId, thirdPartyId, clientType, userContext) { return await recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.getProvider({ thirdPartyId, clientType, tenantId, - userContext, + userContext: utils_1.getUserContext(userContext), }); } static async manuallyCreateOrUpdateUser( @@ -38,7 +39,8 @@ class Wrapper { thirdPartyUserId, email, isVerified, - userContext = {} + shouldAttemptAccountLinkingIfAllowed, + userContext ) { return await recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.manuallyCreateOrUpdateUser({ thirdPartyId, @@ -46,7 +48,8 @@ class Wrapper { email, tenantId: tenantId === undefined ? constants_1.DEFAULT_TENANT_ID : tenantId, isVerified, - userContext, + shouldAttemptAccountLinkingIfAllowed, + userContext: utils_1.getUserContext(userContext), }); } } diff --git a/lib/build/recipe/thirdparty/providers/configUtils.d.ts b/lib/build/recipe/thirdparty/providers/configUtils.d.ts index ab4edbafe..a219eda70 100644 --- a/lib/build/recipe/thirdparty/providers/configUtils.d.ts +++ b/lib/build/recipe/thirdparty/providers/configUtils.d.ts @@ -1,4 +1,5 @@ // @ts-nocheck +import { UserContext } from "../../../types"; import { ProviderClientConfig, ProviderConfig, @@ -14,7 +15,7 @@ export declare function findAndCreateProviderInstance( providers: ProviderInput[], thirdPartyId: string, clientType: string | undefined, - userContext: any + userContext: UserContext ): Promise; export declare function mergeConfig(staticConfig: ProviderConfig, coreConfig: ProviderConfig): ProviderConfig; export declare function mergeProvidersFromCoreAndStatic( diff --git a/lib/build/recipe/thirdparty/recipe.d.ts b/lib/build/recipe/thirdparty/recipe.d.ts index 688c3b2a4..cc79859cf 100644 --- a/lib/build/recipe/thirdparty/recipe.d.ts +++ b/lib/build/recipe/thirdparty/recipe.d.ts @@ -1,6 +1,6 @@ // @ts-nocheck import RecipeModule from "../../recipeModule"; -import { NormalisedAppinfo, APIHandled, RecipeListFunction, HTTPMethod } from "../../types"; +import { NormalisedAppinfo, APIHandled, RecipeListFunction, HTTPMethod, UserContext } from "../../types"; import { TypeInput, TypeNormalisedInput, RecipeInterface, APIInterface, ProviderInput } from "./types"; import STError from "./error"; import NormalisedURLPath from "../../normalisedURLPath"; @@ -32,7 +32,7 @@ export default class Recipe extends RecipeModule { res: BaseResponse, _path: NormalisedURLPath, _method: HTTPMethod, - userContext: any + userContext: UserContext ) => Promise; handleError: (err: STError, _request: BaseRequest, _response: BaseResponse) => Promise; getAllCORSHeaders: () => string[]; diff --git a/lib/build/recipe/thirdparty/recipe.js b/lib/build/recipe/thirdparty/recipe.js index 2c4870278..328126e36 100644 --- a/lib/build/recipe/thirdparty/recipe.js +++ b/lib/build/recipe/thirdparty/recipe.js @@ -33,6 +33,7 @@ const querier_1 = require("../../querier"); const appleRedirect_1 = __importDefault(require("./api/appleRedirect")); const supertokens_js_override_1 = __importDefault(require("supertokens-js-override")); const postSuperTokensInitCallbacks_1 = require("../../postSuperTokensInitCallbacks"); +const recipe_2 = __importDefault(require("../multifactorauth/recipe")); class Recipe extends recipeModule_1.default { constructor(recipeId, appInfo, isInServerlessEnv, config, _recipes, _ingredients) { super(recipeId, appInfo); @@ -120,6 +121,34 @@ class Recipe extends recipeModule_1.default { emailDelivery: undefined, } ); + postSuperTokensInitCallbacks_1.PostSuperTokensInitCallbacks.addPostInitCallback(() => { + const mfaInstance = recipe_2.default.getInstance(); + if (mfaInstance !== undefined) { + mfaInstance.addGetAllFactorsFromOtherRecipesFunc((tenantConfig) => { + if (tenantConfig.thirdParty.enabled === false) { + return { + factorIds: [], + firstFactorIds: [], + }; + } + return { + factorIds: ["thirdparty"], + firstFactorIds: ["thirdparty"], + }; + }); + mfaInstance.addGetFactorsSetupForUserFromOtherRecipes(async (user, tenantConfig) => { + if (tenantConfig.thirdParty.enabled === false) { + return []; + } + for (const loginMethod of user.loginMethods) { + if (loginMethod.recipeId === Recipe.RECIPE_ID) { + return ["thirdparty"]; + } + } + return []; + }); + } + }); return Recipe.instance; } else { throw new Error("ThirdParty recipe has already been initialised. Please check your code for bugs."); diff --git a/lib/build/recipe/thirdparty/recipeImplementation.js b/lib/build/recipe/thirdparty/recipeImplementation.js index 549131827..32fd2c162 100644 --- a/lib/build/recipe/thirdparty/recipeImplementation.js +++ b/lib/build/recipe/thirdparty/recipeImplementation.js @@ -20,6 +20,7 @@ function getRecipeImplementation(querier, providers) { email, isVerified, tenantId, + shouldAttemptAccountLinkingIfAllowed, userContext, }) { let response = await querier.sendPostRequest( @@ -36,11 +37,17 @@ function getRecipeImplementation(querier, providers) { } response.user = new user_1.User(response.user); response.recipeUserId = new recipeUserId_1.default(response.recipeUserId); - await recipe_1.default.getInstance().verifyEmailForRecipeUserIfLinkedAccountsAreVerified({ - user: response.user, - recipeUserId: response.recipeUserId, - userContext, - }); + if ( + shouldAttemptAccountLinkingIfAllowed !== null && shouldAttemptAccountLinkingIfAllowed !== void 0 + ? shouldAttemptAccountLinkingIfAllowed + : true + ) { + await recipe_1.default.getInstance().verifyEmailForRecipeUserIfLinkedAccountsAreVerified({ + user: response.user, + recipeUserId: response.recipeUserId, + userContext, + }); + } // we do this so that we get the updated user (in case the above // function updated the verification status) and can return that response.user = await __1.getUser(response.recipeUserId.getAsString(), userContext); @@ -59,7 +66,6 @@ function getRecipeImplementation(querier, providers) { createdNewRecipeUser: response.createdNewUser, user: response.user, recipeUserId: response.recipeUserId, - isValidFirstFactorForTenant: response.isValidFirstFactorForTenant, }; } let updatedUser = await recipe_1.default.getInstance().createPrimaryUserIdOrLinkAccounts({ @@ -72,7 +78,6 @@ function getRecipeImplementation(querier, providers) { createdNewRecipeUser: response.createdNewUser, user: updatedUser, recipeUserId: response.recipeUserId, - isValidFirstFactorForTenant: response.isValidFirstFactorForTenant, }; }, signInUp: async function ({ @@ -84,6 +89,7 @@ function getRecipeImplementation(querier, providers) { userContext, oAuthTokens, rawUserInfoFromProvider, + shouldAttemptAccountLinkingIfAllowed, }) { let response = await this.manuallyCreateOrUpdateUser({ thirdPartyId, @@ -91,6 +97,7 @@ function getRecipeImplementation(querier, providers) { email, tenantId, isVerified, + shouldAttemptAccountLinkingIfAllowed, userContext, }); if (response.status === "EMAIL_CHANGE_NOT_ALLOWED_ERROR") { diff --git a/lib/build/recipe/thirdparty/types.d.ts b/lib/build/recipe/thirdparty/types.d.ts index d7ad59f5e..a09a02793 100644 --- a/lib/build/recipe/thirdparty/types.d.ts +++ b/lib/build/recipe/thirdparty/types.d.ts @@ -1,10 +1,11 @@ // @ts-nocheck import type { BaseRequest, BaseResponse } from "../../framework"; -import { NormalisedAppinfo } from "../../types"; +import { NormalisedAppinfo, UserContext } from "../../types"; import OverrideableBuilder from "supertokens-js-override"; import { SessionContainerInterface } from "../session/types"; import { GeneralErrorResponse, User } from "../../types"; import RecipeUserId from "../../recipeUserId"; +import { MFAFlowErrors } from "../multifactorauth/types"; export declare type UserInfo = { thirdPartyUserId: string; email?: { @@ -68,7 +69,7 @@ declare type CommonProviderConfig = { [key: string]: any; }; clientConfig: ProviderConfigForClientType; - userContext: any; + userContext: UserContext; }) => Promise; /** * This function is responsible for validating the access token received from the third party provider. @@ -83,19 +84,26 @@ declare type CommonProviderConfig = { validateAccessToken?: (input: { accessToken: string; clientConfig: ProviderConfigForClientType; - userContext: any; + userContext: UserContext; }) => Promise; requireEmail?: boolean; - generateFakeEmail?: (input: { thirdPartyUserId: string; tenantId: string; userContext: any }) => Promise; + generateFakeEmail?: (input: { + thirdPartyUserId: string; + tenantId: string; + userContext: UserContext; + }) => Promise; }; export declare type ProviderConfigForClientType = ProviderClientConfig & CommonProviderConfig; export declare type TypeProvider = { id: string; config: ProviderConfigForClientType; - getConfigForClientType: (input: { clientType?: string; userContext: any }) => Promise; + getConfigForClientType: (input: { + clientType?: string; + userContext: UserContext; + }) => Promise; getAuthorisationRedirectURL: (input: { redirectURIOnProviderDashboard: string; - userContext: any; + userContext: UserContext; }) => Promise<{ urlWithQueryParams: string; pkceCodeVerifier?: string; @@ -106,9 +114,9 @@ export declare type TypeProvider = { redirectURIQueryParams: any; pkceCodeVerifier?: string; }; - userContext: any; + userContext: UserContext; }) => Promise; - getUserInfo: (input: { oAuthTokens: any; userContext: any }) => Promise; + getUserInfo: (input: { oAuthTokens: any; userContext: UserContext }) => Promise; }; export declare type ProviderConfig = CommonProviderConfig & { clients?: ProviderClientConfig[]; @@ -148,7 +156,7 @@ export declare type RecipeInterface = { thirdPartyId: string; tenantId: string; clientType?: string; - userContext: any; + userContext: UserContext; }): Promise; signInUp(input: { thirdPartyId: string; @@ -167,7 +175,8 @@ export declare type RecipeInterface = { }; }; tenantId: string; - userContext: any; + shouldAttemptAccountLinkingIfAllowed?: boolean; + userContext: UserContext; }): Promise< | { status: "OK"; @@ -185,7 +194,6 @@ export declare type RecipeInterface = { [key: string]: any; }; }; - isValidFirstFactorForTenant: boolean | undefined; } | { status: "SIGN_IN_UP_NOT_ALLOWED"; @@ -198,14 +206,14 @@ export declare type RecipeInterface = { email: string; isVerified: boolean; tenantId: string; - userContext: any; + shouldAttemptAccountLinkingIfAllowed?: boolean; + userContext: UserContext; }): Promise< | { status: "OK"; createdNewRecipeUser: boolean; user: User; recipeUserId: RecipeUserId; - isValidFirstFactorForTenant: boolean | undefined; } | { status: "EMAIL_CHANGE_NOT_ALLOWED_ERROR"; @@ -235,7 +243,7 @@ export declare type APIInterface = { redirectURIOnProviderDashboard: string; tenantId: string; options: APIOptions; - userContext: any; + userContext: UserContext; }) => Promise< | { status: "OK"; @@ -251,7 +259,7 @@ export declare type APIInterface = { provider: TypeProvider; tenantId: string; options: APIOptions; - userContext: any; + userContext: UserContext; } & ( | { redirectURIInfo: { @@ -291,6 +299,7 @@ export declare type APIInterface = { status: "SIGN_IN_UP_NOT_ALLOWED"; reason: string; } + | MFAFlowErrors | GeneralErrorResponse >); appleRedirectHandlerPOST: @@ -300,7 +309,7 @@ export declare type APIInterface = { [key: string]: any; }; options: APIOptions; - userContext: any; + userContext: UserContext; }) => Promise); }; export {}; diff --git a/lib/build/recipe/thirdpartyemailpassword/emaildelivery/services/backwardCompatibility/index.d.ts b/lib/build/recipe/thirdpartyemailpassword/emaildelivery/services/backwardCompatibility/index.d.ts index 8ebd5dc12..e37d3e0eb 100644 --- a/lib/build/recipe/thirdpartyemailpassword/emaildelivery/services/backwardCompatibility/index.d.ts +++ b/lib/build/recipe/thirdpartyemailpassword/emaildelivery/services/backwardCompatibility/index.d.ts @@ -1,6 +1,6 @@ // @ts-nocheck import { TypeThirdPartyEmailPasswordEmailDeliveryInput } from "../../../types"; -import { NormalisedAppinfo } from "../../../../../types"; +import { NormalisedAppinfo, UserContext } from "../../../../../types"; import { EmailDeliveryInterface } from "../../../../../ingredients/emaildelivery/types"; export default class BackwardCompatibilityService implements EmailDeliveryInterface { @@ -8,7 +8,7 @@ export default class BackwardCompatibilityService constructor(appInfo: NormalisedAppinfo, isInServerlessEnv: boolean); sendEmail: ( input: TypeThirdPartyEmailPasswordEmailDeliveryInput & { - userContext: any; + userContext: UserContext; } ) => Promise; } diff --git a/lib/build/recipe/thirdpartyemailpassword/emaildelivery/services/smtp/index.d.ts b/lib/build/recipe/thirdpartyemailpassword/emaildelivery/services/smtp/index.d.ts index debae94a2..7d347c9b4 100644 --- a/lib/build/recipe/thirdpartyemailpassword/emaildelivery/services/smtp/index.d.ts +++ b/lib/build/recipe/thirdpartyemailpassword/emaildelivery/services/smtp/index.d.ts @@ -2,12 +2,13 @@ import { TypeInput } from "../../../../../ingredients/emaildelivery/services/smtp"; import { EmailDeliveryInterface } from "../../../../../ingredients/emaildelivery/types"; import { TypeThirdPartyEmailPasswordEmailDeliveryInput } from "../../../types"; +import { UserContext } from "../../../../../types"; export default class SMTPService implements EmailDeliveryInterface { private emailPasswordSMTPService; constructor(config: TypeInput); sendEmail: ( input: TypeThirdPartyEmailPasswordEmailDeliveryInput & { - userContext: any; + userContext: UserContext; } ) => Promise; } diff --git a/lib/build/recipe/thirdpartyemailpassword/index.d.ts b/lib/build/recipe/thirdpartyemailpassword/index.d.ts index 929924e59..6098a7840 100644 --- a/lib/build/recipe/thirdpartyemailpassword/index.d.ts +++ b/lib/build/recipe/thirdpartyemailpassword/index.d.ts @@ -12,7 +12,7 @@ export default class Wrapper { tenantId: string, thirdPartyId: string, clientType: string | undefined, - userContext?: any + userContext?: Record ): Promise; static thirdPartyManuallyCreateOrUpdateUser( tenantId: string, @@ -20,14 +20,14 @@ export default class Wrapper { thirdPartyUserId: string, email: string, isVerified: boolean, - userContext?: any + shouldAttemptAccountLinkingIfAllowed?: boolean, + userContext?: Record ): Promise< | { status: "OK"; createdNewRecipeUser: boolean; user: import("../../types").User; recipeUserId: RecipeUserId; - isValidFirstFactorForTenant: boolean | undefined; } | { status: "EMAIL_CHANGE_NOT_ALLOWED_ERROR"; @@ -42,13 +42,12 @@ export default class Wrapper { tenantId: string, email: string, password: string, - userContext?: any + userContext?: Record ): Promise< | { status: "OK"; user: import("../../types").User; recipeUserId: RecipeUserId; - isValidFirstFactorForTenant: boolean | undefined; } | { status: "EMAIL_ALREADY_EXISTS_ERROR"; @@ -58,13 +57,12 @@ export default class Wrapper { tenantId: string, email: string, password: string, - userContext?: any + userContext?: Record ): Promise< | { status: "OK"; user: import("../../types").User; recipeUserId: RecipeUserId; - isValidFirstFactorForTenant: boolean | undefined; } | { status: "WRONG_CREDENTIALS_ERROR"; @@ -74,7 +72,7 @@ export default class Wrapper { tenantId: string, userId: string, email: string, - userContext?: any + userContext?: Record ): Promise< | { status: "OK"; @@ -88,7 +86,7 @@ export default class Wrapper { tenantId: string, token: string, newPassword: string, - userContext?: any + userContext?: Record ): Promise< | { status: "RESET_PASSWORD_INVALID_TOKEN_ERROR"; @@ -108,7 +106,7 @@ export default class Wrapper { static consumePasswordResetToken( tenantId: string, token: string, - userContext?: any + userContext?: Record ): Promise< | { status: "OK"; @@ -123,7 +121,7 @@ export default class Wrapper { recipeUserId: RecipeUserId; email?: string; password?: string; - userContext?: any; + userContext?: Record; applyPasswordPolicy?: boolean; tenantIdForPasswordPolicy?: string; }): Promise< @@ -143,7 +141,7 @@ export default class Wrapper { tenantId: string, userId: string, email: string, - userContext?: any + userContext?: Record ): Promise< | { status: "OK"; @@ -157,13 +155,13 @@ export default class Wrapper { tenantId: string, userId: string, email: string, - userContext?: any + userContext?: Record ): Promise<{ status: "OK" | "UNKNOWN_USER_ID_ERROR"; }>; static sendEmail( input: TypeEmailPasswordEmailDeliveryInput & { - userContext?: any; + userContext?: Record; } ): Promise; } diff --git a/lib/build/recipe/thirdpartyemailpassword/index.js b/lib/build/recipe/thirdpartyemailpassword/index.js index d0d0ed1d9..7926fc62b 100644 --- a/lib/build/recipe/thirdpartyemailpassword/index.js +++ b/lib/build/recipe/thirdpartyemailpassword/index.js @@ -26,13 +26,14 @@ const recipeUserId_1 = __importDefault(require("../../recipeUserId")); const constants_1 = require("../multitenancy/constants"); const utils_1 = require("../emailpassword/utils"); const __1 = require("../.."); +const utils_2 = require("../../utils"); class Wrapper { - static async thirdPartyGetProvider(tenantId, thirdPartyId, clientType, userContext = {}) { + static async thirdPartyGetProvider(tenantId, thirdPartyId, clientType, userContext) { return await recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.thirdPartyGetProvider({ thirdPartyId, clientType, tenantId, - userContext, + userContext: utils_2.getUserContext(userContext), }); } static thirdPartyManuallyCreateOrUpdateUser( @@ -41,7 +42,8 @@ class Wrapper { thirdPartyUserId, email, isVerified, - userContext = {} + shouldAttemptAccountLinkingIfAllowed, + userContext ) { return recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.thirdPartyManuallyCreateOrUpdateUser({ thirdPartyId, @@ -49,31 +51,32 @@ class Wrapper { email, isVerified, tenantId, - userContext, + shouldAttemptAccountLinkingIfAllowed, + userContext: utils_2.getUserContext(userContext), }); } - static emailPasswordSignUp(tenantId, email, password, userContext = {}) { + static emailPasswordSignUp(tenantId, email, password, userContext) { return recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.emailPasswordSignUp({ email, password, tenantId, - userContext, + userContext: utils_2.getUserContext(userContext), }); } - static emailPasswordSignIn(tenantId, email, password, userContext = {}) { + static emailPasswordSignIn(tenantId, email, password, userContext) { return recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.emailPasswordSignIn({ email, password, tenantId, - userContext, + userContext: utils_2.getUserContext(userContext), }); } - static createResetPasswordToken(tenantId, userId, email, userContext = {}) { + static createResetPasswordToken(tenantId, userId, email, userContext) { return recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.createResetPasswordToken({ userId, email, tenantId, - userContext, + userContext: utils_2.getUserContext(userContext), }); } static async resetPasswordUsingToken(tenantId, token, newPassword, userContext) { @@ -89,18 +92,17 @@ class Wrapper { userContext, }); } - static consumePasswordResetToken(tenantId, token, userContext = {}) { + static consumePasswordResetToken(tenantId, token, userContext) { return recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.consumePasswordResetToken({ token, tenantId, - userContext, + userContext: utils_2.getUserContext(userContext), }); } static updateEmailOrPassword(input) { - var _a; return recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.updateEmailOrPassword( Object.assign(Object.assign({}, input), { - userContext: (_a = input.userContext) !== null && _a !== void 0 ? _a : {}, + userContext: utils_2.getUserContext(input.userContext), tenantIdForPasswordPolicy: input.tenantIdForPasswordPolicy === undefined ? constants_1.DEFAULT_TENANT_ID @@ -108,7 +110,8 @@ class Wrapper { }) ); } - static async createResetPasswordLink(tenantId, userId, email, userContext = {}) { + static async createResetPasswordLink(tenantId, userId, email, userContext) { + const ctx = utils_2.getUserContext(userContext); let token = await exports.createResetPasswordToken(tenantId, userId, email, userContext); if (token.status === "UNKNOWN_USER_ID_ERROR") { return token; @@ -121,12 +124,12 @@ class Wrapper { recipeId: recipeInstance.getRecipeId(), token: token.token, tenantId, - request: __1.getRequestFromUserContext(userContext), - userContext, + request: __1.getRequestFromUserContext(ctx), + userContext: ctx, }), }; } - static async sendResetPasswordEmail(tenantId, userId, email, userContext = {}) { + static async sendResetPasswordEmail(tenantId, userId, email, userContext) { const user = await __1.getUser(userId, userContext); if (!user) { return { status: "UNKNOWN_USER_ID_ERROR" }; @@ -155,10 +158,9 @@ class Wrapper { }; } static async sendEmail(input) { - var _a; return await recipe_1.default.getInstanceOrThrowError().emailDelivery.ingredientInterfaceImpl.sendEmail( Object.assign(Object.assign({}, input), { - userContext: (_a = input.userContext) !== null && _a !== void 0 ? _a : {}, + userContext: utils_2.getUserContext(input.userContext), tenantId: input.tenantId === undefined ? constants_1.DEFAULT_TENANT_ID : input.tenantId, }) ); diff --git a/lib/build/recipe/thirdpartyemailpassword/recipe.d.ts b/lib/build/recipe/thirdpartyemailpassword/recipe.d.ts index 3103cfebb..0241fb715 100644 --- a/lib/build/recipe/thirdpartyemailpassword/recipe.d.ts +++ b/lib/build/recipe/thirdpartyemailpassword/recipe.d.ts @@ -1,6 +1,6 @@ // @ts-nocheck import RecipeModule from "../../recipeModule"; -import { NormalisedAppinfo, APIHandled, RecipeListFunction, HTTPMethod } from "../../types"; +import { NormalisedAppinfo, APIHandled, RecipeListFunction, HTTPMethod, UserContext } from "../../types"; import EmailPasswordRecipe from "../emailpassword/recipe"; import ThirdPartyRecipe from "../thirdparty/recipe"; import type { BaseRequest, BaseResponse } from "../../framework"; @@ -50,7 +50,7 @@ export default class Recipe extends RecipeModule { res: BaseResponse, path: NormalisedURLPath, method: HTTPMethod, - userContext: any + userContext: UserContext ) => Promise; handleError: ( err: STErrorEmailPassword | STErrorThirdParty, diff --git a/lib/build/recipe/thirdpartyemailpassword/recipeImplementation/thirdPartyRecipeImplementation.js b/lib/build/recipe/thirdpartyemailpassword/recipeImplementation/thirdPartyRecipeImplementation.js index 476108c02..0202e58b6 100644 --- a/lib/build/recipe/thirdpartyemailpassword/recipeImplementation/thirdPartyRecipeImplementation.js +++ b/lib/build/recipe/thirdpartyemailpassword/recipeImplementation/thirdPartyRecipeImplementation.js @@ -18,7 +18,6 @@ function getRecipeInterface(recipeInterface) { createdNewRecipeUser: result.createdNewRecipeUser, user: result.user, recipeUserId: result.recipeUserId, - isValidFirstFactorForTenant: result.isValidFirstFactorForTenant, }; }, getProvider: async function (input) { diff --git a/lib/build/recipe/thirdpartyemailpassword/types.d.ts b/lib/build/recipe/thirdpartyemailpassword/types.d.ts index 4814c72f0..db1d6d6e7 100644 --- a/lib/build/recipe/thirdpartyemailpassword/types.d.ts +++ b/lib/build/recipe/thirdpartyemailpassword/types.d.ts @@ -19,8 +19,9 @@ import { TypeInput as EmailDeliveryTypeInput, TypeInputWithService as EmailDeliveryTypeInputWithService, } from "../../ingredients/emaildelivery/types"; -import { GeneralErrorResponse, User as GlobalUser, User } from "../../types"; +import { GeneralErrorResponse, User as GlobalUser, User, UserContext } from "../../types"; import RecipeUserId from "../../recipeUserId"; +import { MFAFlowErrors } from "../multifactorauth/types"; export declare type TypeInputSignUp = { formFields?: TypeInputFormField[]; }; @@ -58,7 +59,7 @@ export declare type RecipeInterface = { thirdPartyId: string; clientType?: string; tenantId: string; - userContext: any; + userContext: UserContext; }): Promise; thirdPartySignInUp(input: { thirdPartyId: string; @@ -77,7 +78,8 @@ export declare type RecipeInterface = { }; }; tenantId: string; - userContext: any; + shouldAttemptAccountLinkingIfAllowed?: boolean; + userContext: UserContext; }): Promise< | { status: "OK"; @@ -95,7 +97,6 @@ export declare type RecipeInterface = { [key: string]: any; }; }; - isValidFirstFactorForTenant: boolean | undefined; } | { status: "SIGN_IN_UP_NOT_ALLOWED"; @@ -108,14 +109,14 @@ export declare type RecipeInterface = { email: string; isVerified: boolean; tenantId: string; - userContext: any; + shouldAttemptAccountLinkingIfAllowed?: boolean; + userContext: UserContext; }): Promise< | { status: "OK"; createdNewRecipeUser: boolean; user: GlobalUser; recipeUserId: RecipeUserId; - isValidFirstFactorForTenant: boolean | undefined; } | { status: "EMAIL_CHANGE_NOT_ALLOWED_ERROR"; @@ -130,13 +131,12 @@ export declare type RecipeInterface = { email: string; password: string; tenantId: string; - userContext: any; + userContext: UserContext; }): Promise< | { status: "OK"; user: GlobalUser; recipeUserId: RecipeUserId; - isValidFirstFactorForTenant: boolean | undefined; } | { status: "EMAIL_ALREADY_EXISTS_ERROR"; @@ -145,13 +145,12 @@ export declare type RecipeInterface = { createNewEmailPasswordRecipeUser(input: { email: string; password: string; - userContext: any; + userContext: UserContext; }): Promise< | { status: "OK"; user: GlobalUser; recipeUserId: RecipeUserId; - isValidFirstFactorForTenant: boolean | undefined; } | { status: "EMAIL_ALREADY_EXISTS_ERROR"; @@ -161,13 +160,12 @@ export declare type RecipeInterface = { email: string; password: string; tenantId: string; - userContext: any; + userContext: UserContext; }): Promise< | { status: "OK"; user: GlobalUser; recipeUserId: RecipeUserId; - isValidFirstFactorForTenant: boolean | undefined; } | { status: "WRONG_CREDENTIALS_ERROR"; @@ -177,7 +175,7 @@ export declare type RecipeInterface = { userId: string; email: string; tenantId: string; - userContext: any; + userContext: UserContext; }): Promise< | { status: "OK"; @@ -190,7 +188,7 @@ export declare type RecipeInterface = { consumePasswordResetToken(input: { token: string; tenantId: string; - userContext: any; + userContext: UserContext; }): Promise< | { status: "OK"; @@ -205,7 +203,7 @@ export declare type RecipeInterface = { recipeUserId: RecipeUserId; email?: string; password?: string; - userContext: any; + userContext: UserContext; applyPasswordPolicy?: boolean; tenantIdForPasswordPolicy: string; }): Promise< @@ -232,7 +230,7 @@ export declare type APIInterface = { redirectURIOnProviderDashboard: string; tenantId: string; options: ThirdPartyAPIOptions; - userContext: any; + userContext: UserContext; }) => Promise< | { status: "OK"; @@ -247,7 +245,7 @@ export declare type APIInterface = { email: string; tenantId: string; options: EmailPasswordAPIOptions; - userContext: any; + userContext: UserContext; }) => Promise< | { status: "OK"; @@ -264,7 +262,7 @@ export declare type APIInterface = { }[]; tenantId: string; options: EmailPasswordAPIOptions; - userContext: any; + userContext: UserContext; }) => Promise< | { status: "OK"; @@ -285,7 +283,7 @@ export declare type APIInterface = { token: string; tenantId: string; options: EmailPasswordAPIOptions; - userContext: any; + userContext: UserContext; }) => Promise< | { status: "OK"; @@ -308,7 +306,7 @@ export declare type APIInterface = { provider: TypeProvider; tenantId: string; options: ThirdPartyAPIOptions; - userContext: any; + userContext: UserContext; } & ( | { redirectURIInfo: { @@ -348,6 +346,7 @@ export declare type APIInterface = { status: "SIGN_IN_UP_NOT_ALLOWED"; reason: string; } + | MFAFlowErrors | GeneralErrorResponse >); emailPasswordSignInPOST: @@ -359,7 +358,7 @@ export declare type APIInterface = { }[]; tenantId: string; options: EmailPasswordAPIOptions; - userContext: any; + userContext: UserContext; }) => Promise< | { status: "OK"; @@ -373,6 +372,7 @@ export declare type APIInterface = { | { status: "WRONG_CREDENTIALS_ERROR"; } + | MFAFlowErrors | GeneralErrorResponse >); emailPasswordSignUpPOST: @@ -384,7 +384,7 @@ export declare type APIInterface = { }[]; tenantId: string; options: EmailPasswordAPIOptions; - userContext: any; + userContext: UserContext; }) => Promise< | { status: "OK"; @@ -398,6 +398,7 @@ export declare type APIInterface = { | { status: "EMAIL_ALREADY_EXISTS_ERROR"; } + | MFAFlowErrors | GeneralErrorResponse >); appleRedirectHandlerPOST: @@ -405,7 +406,7 @@ export declare type APIInterface = { | ((input: { formPostInfoFromProvider: any; options: ThirdPartyAPIOptions; - userContext: any; + userContext: UserContext; }) => Promise); }; export declare type TypeThirdPartyEmailPasswordEmailDeliveryInput = TypeEmailPasswordEmailDeliveryInput; diff --git a/lib/build/recipe/thirdpartypasswordless/emaildelivery/services/backwardCompatibility/index.d.ts b/lib/build/recipe/thirdpartypasswordless/emaildelivery/services/backwardCompatibility/index.d.ts index 29bbefd07..9bc9072b2 100644 --- a/lib/build/recipe/thirdpartypasswordless/emaildelivery/services/backwardCompatibility/index.d.ts +++ b/lib/build/recipe/thirdpartypasswordless/emaildelivery/services/backwardCompatibility/index.d.ts @@ -1,6 +1,6 @@ // @ts-nocheck import { TypeThirdPartyPasswordlessEmailDeliveryInput } from "../../../types"; -import { NormalisedAppinfo } from "../../../../../types"; +import { NormalisedAppinfo, UserContext } from "../../../../../types"; import { EmailDeliveryInterface } from "../../../../../ingredients/emaildelivery/types"; export default class BackwardCompatibilityService implements EmailDeliveryInterface { @@ -8,7 +8,7 @@ export default class BackwardCompatibilityService constructor(appInfo: NormalisedAppinfo); sendEmail: ( input: TypeThirdPartyPasswordlessEmailDeliveryInput & { - userContext: any; + userContext: UserContext; } ) => Promise; } diff --git a/lib/build/recipe/thirdpartypasswordless/emaildelivery/services/smtp/index.d.ts b/lib/build/recipe/thirdpartypasswordless/emaildelivery/services/smtp/index.d.ts index ebb0eb32a..879b400f6 100644 --- a/lib/build/recipe/thirdpartypasswordless/emaildelivery/services/smtp/index.d.ts +++ b/lib/build/recipe/thirdpartypasswordless/emaildelivery/services/smtp/index.d.ts @@ -2,13 +2,14 @@ import { ServiceInterface, TypeInput } from "../../../../../ingredients/emaildelivery/services/smtp"; import { TypeThirdPartyPasswordlessEmailDeliveryInput } from "../../../types"; import { EmailDeliveryInterface } from "../../../../../ingredients/emaildelivery/types"; +import { UserContext } from "../../../../../types"; export default class SMTPService implements EmailDeliveryInterface { serviceImpl: ServiceInterface; private passwordlessSMTPService; constructor(config: TypeInput); sendEmail: ( input: TypeThirdPartyPasswordlessEmailDeliveryInput & { - userContext: any; + userContext: UserContext; } ) => Promise; } diff --git a/lib/build/recipe/thirdpartypasswordless/index.d.ts b/lib/build/recipe/thirdpartypasswordless/index.d.ts index a53328036..b9b50464e 100644 --- a/lib/build/recipe/thirdpartypasswordless/index.d.ts +++ b/lib/build/recipe/thirdpartypasswordless/index.d.ts @@ -18,7 +18,7 @@ export default class Wrapper { tenantId: string, thirdPartyId: string, clientType: string | undefined, - userContext?: any + userContext?: Record ): Promise; static thirdPartyManuallyCreateOrUpdateUser( tenantId: string, @@ -26,14 +26,14 @@ export default class Wrapper { thirdPartyUserId: string, email: string, isVerified: boolean, - userContext?: any + shouldAttemptAccountLinkingIfAllowed?: boolean, + userContext?: Record ): Promise< | { status: "OK"; createdNewRecipeUser: boolean; user: import("../../types").User; recipeUserId: RecipeUserId; - isValidFirstFactorForTenant: boolean | undefined; } | { status: "EMAIL_CHANGE_NOT_ALLOWED_ERROR"; @@ -55,7 +55,7 @@ export default class Wrapper { ) & { userInputCode?: string; tenantId: string; - userContext?: any; + userContext?: Record; } ): Promise<{ status: "OK"; @@ -71,7 +71,7 @@ export default class Wrapper { deviceId: string; userInputCode?: string; tenantId: string; - userContext?: any; + userContext?: Record; }): Promise< | { status: "OK"; @@ -94,13 +94,13 @@ export default class Wrapper { userInputCode: string; deviceId: string; tenantId: string; - userContext?: any; + userContext?: Record; } | { preAuthSessionId: string; linkCode: string; tenantId: string; - userContext?: any; + userContext?: Record; } ): Promise< | { @@ -108,7 +108,6 @@ export default class Wrapper { createdNewRecipeUser: boolean; user: import("../../types").User; recipeUserId: RecipeUserId; - isValidFirstFactorForTenant: boolean | undefined; } | { status: "INCORRECT_USER_INPUT_CODE_ERROR" | "EXPIRED_USER_INPUT_CODE_ERROR"; @@ -123,7 +122,7 @@ export default class Wrapper { recipeUserId: RecipeUserId; email?: string | null; phoneNumber?: string | null; - userContext?: any; + userContext?: Record; }): Promise< | { status: @@ -142,12 +141,12 @@ export default class Wrapper { | { email: string; tenantId: string; - userContext?: any; + userContext?: Record; } | { phoneNumber: string; tenantId: string; - userContext?: any; + userContext?: Record; } ): Promise<{ status: "OK"; @@ -155,41 +154,41 @@ export default class Wrapper { static revokeCode(input: { codeId: string; tenantId: string; - userContext?: any; + userContext?: Record; }): Promise<{ status: "OK"; }>; static listCodesByEmail(input: { email: string; tenantId: string; - userContext?: any; + userContext?: Record; }): Promise; static listCodesByPhoneNumber(input: { phoneNumber: string; tenantId: string; - userContext?: any; + userContext?: Record; }): Promise; static listCodesByDeviceId(input: { deviceId: string; tenantId: string; - userContext?: any; + userContext?: Record; }): Promise; static listCodesByPreAuthSessionId(input: { preAuthSessionId: string; tenantId: string; - userContext?: any; + userContext?: Record; }): Promise; static createMagicLink( input: | { email: string; tenantId: string; - userContext?: any; + userContext?: Record; } | { phoneNumber: string; tenantId: string; - userContext?: any; + userContext?: Record; } ): Promise; static passwordlessSignInUp( @@ -197,28 +196,29 @@ export default class Wrapper { | { email: string; tenantId: string; - userContext?: any; + shouldAttemptAccountLinkingIfAllowed?: boolean; + userContext?: Record; } | { phoneNumber: string; tenantId: string; - userContext?: any; + shouldAttemptAccountLinkingIfAllowed?: boolean; + userContext?: Record; } ): Promise<{ status: string; createdNewRecipeUser: boolean; recipeUserId: RecipeUserId; user: import("../../types").User; - isValidFirstFactorForTenant: boolean | undefined; }>; static sendEmail( input: TypeThirdPartyPasswordlessEmailDeliveryInput & { - userContext?: any; + userContext?: Record; } ): Promise; static sendSms( input: TypePasswordlessSmsDeliveryInput & { - userContext?: any; + userContext?: Record; } ): Promise; } diff --git a/lib/build/recipe/thirdpartypasswordless/index.js b/lib/build/recipe/thirdpartypasswordless/index.js index a53f843e9..192040f5f 100644 --- a/lib/build/recipe/thirdpartypasswordless/index.js +++ b/lib/build/recipe/thirdpartypasswordless/index.js @@ -23,13 +23,14 @@ exports.sendSms = exports.sendEmail = exports.createMagicLink = exports.revokeCo const recipe_1 = __importDefault(require("./recipe")); const error_1 = __importDefault(require("./error")); const __1 = require("../.."); +const utils_1 = require("../../utils"); class Wrapper { - static async thirdPartyGetProvider(tenantId, thirdPartyId, clientType, userContext = {}) { + static async thirdPartyGetProvider(tenantId, thirdPartyId, clientType, userContext) { return await recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.thirdPartyGetProvider({ thirdPartyId, tenantId, clientType, - userContext, + userContext: utils_1.getUserContext(userContext), }); } static thirdPartyManuallyCreateOrUpdateUser( @@ -38,7 +39,8 @@ class Wrapper { thirdPartyUserId, email, isVerified, - userContext = {} + shouldAttemptAccountLinkingIfAllowed, + userContext ) { return recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.thirdPartyManuallyCreateOrUpdateUser({ thirdPartyId, @@ -46,121 +48,109 @@ class Wrapper { email, isVerified, tenantId, - userContext, + shouldAttemptAccountLinkingIfAllowed, + userContext: utils_1.getUserContext(userContext), }); } static createCode(input) { - var _a; - return recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.createCode( - Object.assign(Object.assign({}, input), { - userContext: (_a = input.userContext) !== null && _a !== void 0 ? _a : {}, - }) - ); + return recipe_1.default + .getInstanceOrThrowError() + .recipeInterfaceImpl.createCode( + Object.assign(Object.assign({}, input), { userContext: utils_1.getUserContext(input.userContext) }) + ); } static createNewCodeForDevice(input) { - var _a; - return recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.createNewCodeForDevice( - Object.assign(Object.assign({}, input), { - userContext: (_a = input.userContext) !== null && _a !== void 0 ? _a : {}, - }) - ); + return recipe_1.default + .getInstanceOrThrowError() + .recipeInterfaceImpl.createNewCodeForDevice( + Object.assign(Object.assign({}, input), { userContext: utils_1.getUserContext(input.userContext) }) + ); } static consumeCode(input) { - var _a; - return recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.consumeCode( - Object.assign(Object.assign({}, input), { - userContext: (_a = input.userContext) !== null && _a !== void 0 ? _a : {}, - }) - ); + return recipe_1.default + .getInstanceOrThrowError() + .recipeInterfaceImpl.consumeCode( + Object.assign(Object.assign({}, input), { userContext: utils_1.getUserContext(input.userContext) }) + ); } static updatePasswordlessUser(input) { - var _a; - return recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.updatePasswordlessUser( - Object.assign(Object.assign({}, input), { - userContext: (_a = input.userContext) !== null && _a !== void 0 ? _a : {}, - }) - ); + return recipe_1.default + .getInstanceOrThrowError() + .recipeInterfaceImpl.updatePasswordlessUser( + Object.assign(Object.assign({}, input), { userContext: utils_1.getUserContext(input.userContext) }) + ); } static revokeAllCodes(input) { - var _a; - return recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.revokeAllCodes( - Object.assign(Object.assign({}, input), { - userContext: (_a = input.userContext) !== null && _a !== void 0 ? _a : {}, - }) - ); + return recipe_1.default + .getInstanceOrThrowError() + .recipeInterfaceImpl.revokeAllCodes( + Object.assign(Object.assign({}, input), { userContext: utils_1.getUserContext(input.userContext) }) + ); } static revokeCode(input) { - var _a; - return recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.revokeCode( - Object.assign(Object.assign({}, input), { - userContext: (_a = input.userContext) !== null && _a !== void 0 ? _a : {}, - }) - ); + return recipe_1.default + .getInstanceOrThrowError() + .recipeInterfaceImpl.revokeCode( + Object.assign(Object.assign({}, input), { userContext: utils_1.getUserContext(input.userContext) }) + ); } static listCodesByEmail(input) { - var _a; - return recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.listCodesByEmail( - Object.assign(Object.assign({}, input), { - userContext: (_a = input.userContext) !== null && _a !== void 0 ? _a : {}, - }) - ); + return recipe_1.default + .getInstanceOrThrowError() + .recipeInterfaceImpl.listCodesByEmail( + Object.assign(Object.assign({}, input), { userContext: utils_1.getUserContext(input.userContext) }) + ); } static listCodesByPhoneNumber(input) { - var _a; - return recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.listCodesByPhoneNumber( - Object.assign(Object.assign({}, input), { - userContext: (_a = input.userContext) !== null && _a !== void 0 ? _a : {}, - }) - ); + return recipe_1.default + .getInstanceOrThrowError() + .recipeInterfaceImpl.listCodesByPhoneNumber( + Object.assign(Object.assign({}, input), { userContext: utils_1.getUserContext(input.userContext) }) + ); } static listCodesByDeviceId(input) { - var _a; - return recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.listCodesByDeviceId( - Object.assign(Object.assign({}, input), { - userContext: (_a = input.userContext) !== null && _a !== void 0 ? _a : {}, - }) - ); + return recipe_1.default + .getInstanceOrThrowError() + .recipeInterfaceImpl.listCodesByDeviceId( + Object.assign(Object.assign({}, input), { userContext: utils_1.getUserContext(input.userContext) }) + ); } static listCodesByPreAuthSessionId(input) { - var _a; - return recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.listCodesByPreAuthSessionId( - Object.assign(Object.assign({}, input), { - userContext: (_a = input.userContext) !== null && _a !== void 0 ? _a : {}, - }) - ); + return recipe_1.default + .getInstanceOrThrowError() + .recipeInterfaceImpl.listCodesByPreAuthSessionId( + Object.assign(Object.assign({}, input), { userContext: utils_1.getUserContext(input.userContext) }) + ); } static createMagicLink(input) { - var _a; + const ctx = utils_1.getUserContext(input.userContext); return recipe_1.default.getInstanceOrThrowError().passwordlessRecipe.createMagicLink( Object.assign(Object.assign({}, input), { - request: __1.getRequestFromUserContext(input.userContext), - userContext: (_a = input.userContext) !== null && _a !== void 0 ? _a : {}, + request: __1.getRequestFromUserContext(ctx), + userContext: utils_1.getUserContext(ctx), }) ); } static passwordlessSignInUp(input) { - var _a; - return recipe_1.default.getInstanceOrThrowError().passwordlessRecipe.signInUp( - Object.assign(Object.assign({}, input), { - userContext: (_a = input.userContext) !== null && _a !== void 0 ? _a : {}, - }) - ); + return recipe_1.default + .getInstanceOrThrowError() + .passwordlessRecipe.signInUp( + Object.assign(Object.assign({}, input), { userContext: utils_1.getUserContext(input.userContext) }) + ); } static async sendEmail(input) { - var _a; - return await recipe_1.default.getInstanceOrThrowError().emailDelivery.ingredientInterfaceImpl.sendEmail( - Object.assign(Object.assign({}, input), { - userContext: (_a = input.userContext) !== null && _a !== void 0 ? _a : {}, - }) - ); + return await recipe_1.default + .getInstanceOrThrowError() + .emailDelivery.ingredientInterfaceImpl.sendEmail( + Object.assign(Object.assign({}, input), { userContext: utils_1.getUserContext(input.userContext) }) + ); } static async sendSms(input) { - var _a; - return await recipe_1.default.getInstanceOrThrowError().smsDelivery.ingredientInterfaceImpl.sendSms( - Object.assign(Object.assign({}, input), { - userContext: (_a = input.userContext) !== null && _a !== void 0 ? _a : {}, - }) - ); + return await recipe_1.default + .getInstanceOrThrowError() + .smsDelivery.ingredientInterfaceImpl.sendSms( + Object.assign(Object.assign({}, input), { userContext: utils_1.getUserContext(input.userContext) }) + ); } } exports.default = Wrapper; diff --git a/lib/build/recipe/thirdpartypasswordless/recipe.d.ts b/lib/build/recipe/thirdpartypasswordless/recipe.d.ts index 1e88d13b5..b80a18638 100644 --- a/lib/build/recipe/thirdpartypasswordless/recipe.d.ts +++ b/lib/build/recipe/thirdpartypasswordless/recipe.d.ts @@ -1,6 +1,6 @@ // @ts-nocheck import RecipeModule from "../../recipeModule"; -import { NormalisedAppinfo, APIHandled, RecipeListFunction, HTTPMethod } from "../../types"; +import { NormalisedAppinfo, APIHandled, RecipeListFunction, HTTPMethod, UserContext } from "../../types"; import PasswordlessRecipe from "../passwordless/recipe"; import ThirdPartyRecipe from "../thirdparty/recipe"; import type { BaseRequest, BaseResponse } from "../../framework"; @@ -54,7 +54,7 @@ export default class Recipe extends RecipeModule { res: BaseResponse, path: NormalisedURLPath, method: HTTPMethod, - userContext: any + userContext: UserContext ) => Promise; handleError: ( err: STErrorPasswordless | STErrorThirdParty, diff --git a/lib/build/recipe/thirdpartypasswordless/recipeImplementation/thirdPartyRecipeImplementation.js b/lib/build/recipe/thirdpartypasswordless/recipeImplementation/thirdPartyRecipeImplementation.js index 09e983b65..1b2b14eeb 100644 --- a/lib/build/recipe/thirdpartypasswordless/recipeImplementation/thirdPartyRecipeImplementation.js +++ b/lib/build/recipe/thirdpartypasswordless/recipeImplementation/thirdPartyRecipeImplementation.js @@ -18,7 +18,6 @@ function getRecipeInterface(recipeInterface) { createdNewRecipeUser: result.createdNewRecipeUser, recipeUserId: result.recipeUserId, user: result.user, - isValidFirstFactorForTenant: result.isValidFirstFactorForTenant, }; }, getProvider: async function (input) { diff --git a/lib/build/recipe/thirdpartypasswordless/smsdelivery/services/backwardCompatibility/index.d.ts b/lib/build/recipe/thirdpartypasswordless/smsdelivery/services/backwardCompatibility/index.d.ts index 275e3e17f..c245db048 100644 --- a/lib/build/recipe/thirdpartypasswordless/smsdelivery/services/backwardCompatibility/index.d.ts +++ b/lib/build/recipe/thirdpartypasswordless/smsdelivery/services/backwardCompatibility/index.d.ts @@ -1,13 +1,14 @@ // @ts-nocheck import { TypeThirdPartyPasswordlessSmsDeliveryInput } from "../../../types"; import { SmsDeliveryInterface } from "../../../../../ingredients/smsdelivery/types"; +import { UserContext } from "../../../../../types"; export default class BackwardCompatibilityService implements SmsDeliveryInterface { private passwordlessBackwardCompatibilityService; constructor(); sendSms: ( input: TypeThirdPartyPasswordlessSmsDeliveryInput & { - userContext: any; + userContext: UserContext; } ) => Promise; } diff --git a/lib/build/recipe/thirdpartypasswordless/smsdelivery/services/supertokens/index.d.ts b/lib/build/recipe/thirdpartypasswordless/smsdelivery/services/supertokens/index.d.ts index 2be198af4..25c5d76d7 100644 --- a/lib/build/recipe/thirdpartypasswordless/smsdelivery/services/supertokens/index.d.ts +++ b/lib/build/recipe/thirdpartypasswordless/smsdelivery/services/supertokens/index.d.ts @@ -1,12 +1,13 @@ // @ts-nocheck import { SmsDeliveryInterface } from "../../../../../ingredients/smsdelivery/types"; import { TypeThirdPartyPasswordlessSmsDeliveryInput } from "../../../types"; +import { UserContext } from "../../../../../types"; export default class SupertokensService implements SmsDeliveryInterface { private passwordlessSupertokensService; constructor(apiKey: string); sendSms: ( input: TypeThirdPartyPasswordlessSmsDeliveryInput & { - userContext: any; + userContext: UserContext; } ) => Promise; } diff --git a/lib/build/recipe/thirdpartypasswordless/smsdelivery/services/twilio/index.d.ts b/lib/build/recipe/thirdpartypasswordless/smsdelivery/services/twilio/index.d.ts index ec73adb1f..1db891a20 100644 --- a/lib/build/recipe/thirdpartypasswordless/smsdelivery/services/twilio/index.d.ts +++ b/lib/build/recipe/thirdpartypasswordless/smsdelivery/services/twilio/index.d.ts @@ -2,12 +2,13 @@ import { TypeInput } from "../../../../../ingredients/smsdelivery/services/twilio"; import { SmsDeliveryInterface } from "../../../../../ingredients/smsdelivery/types"; import { TypeThirdPartyPasswordlessSmsDeliveryInput } from "../../../types"; +import { UserContext } from "../../../../../types"; export default class TwilioService implements SmsDeliveryInterface { private passwordlessTwilioService; constructor(config: TypeInput); sendSms: ( input: TypeThirdPartyPasswordlessSmsDeliveryInput & { - userContext: any; + userContext: UserContext; } ) => Promise; } diff --git a/lib/build/recipe/thirdpartypasswordless/types.d.ts b/lib/build/recipe/thirdpartypasswordless/types.d.ts index 87e3ff377..ad463f07a 100644 --- a/lib/build/recipe/thirdpartypasswordless/types.d.ts +++ b/lib/build/recipe/thirdpartypasswordless/types.d.ts @@ -23,8 +23,9 @@ import { TypeInput as SmsDeliveryTypeInput, TypeInputWithService as SmsDeliveryTypeInputWithService, } from "../../ingredients/smsdelivery/types"; -import { GeneralErrorResponse, User } from "../../types"; +import { GeneralErrorResponse, User, UserContext } from "../../types"; import RecipeUserId from "../../recipeUserId"; +import { MFAFlowErrors } from "../multifactorauth/types"; export declare type DeviceType = DeviceTypeOriginal; export declare type TypeInput = ( | { @@ -51,7 +52,7 @@ export declare type TypeInput = ( smsDelivery?: SmsDeliveryTypeInput; providers?: ProviderInput[]; flowType: "USER_INPUT_CODE" | "MAGIC_LINK" | "USER_INPUT_CODE_AND_MAGIC_LINK"; - getCustomUserInputCode?: (tenantId: string, userContext: any) => Promise | string; + getCustomUserInputCode?: (tenantId: string, userContext: UserContext) => Promise | string; override?: { functions?: ( originalImplementation: RecipeInterface, @@ -82,7 +83,7 @@ export declare type TypeNormalisedInput = ( } ) & { flowType: "USER_INPUT_CODE" | "MAGIC_LINK" | "USER_INPUT_CODE_AND_MAGIC_LINK"; - getCustomUserInputCode?: (tenantId: string, userContext: any) => Promise | string; + getCustomUserInputCode?: (tenantId: string, userContext: UserContext) => Promise | string; providers: ProviderInput[]; getEmailDeliveryConfig: ( recipeImpl: RecipeInterface, @@ -115,7 +116,8 @@ export declare type RecipeInterface = { }; }; tenantId: string; - userContext: any; + shouldAttemptAccountLinkingIfAllowed?: boolean; + userContext: UserContext; }): Promise< | { status: "OK"; @@ -133,7 +135,6 @@ export declare type RecipeInterface = { [key: string]: any; }; }; - isValidFirstFactorForTenant: boolean | undefined; } | { status: "SIGN_IN_UP_NOT_ALLOWED"; @@ -146,14 +147,14 @@ export declare type RecipeInterface = { email: string; isVerified: boolean; tenantId: string; - userContext: any; + shouldAttemptAccountLinkingIfAllowed?: boolean; + userContext: UserContext; }): Promise< | { status: "OK"; createdNewRecipeUser: boolean; user: User; recipeUserId: RecipeUserId; - isValidFirstFactorForTenant: boolean | undefined; } | { status: "EMAIL_CHANGE_NOT_ALLOWED_ERROR"; @@ -168,7 +169,7 @@ export declare type RecipeInterface = { thirdPartyId: string; clientType?: string; tenantId: string; - userContext: any; + userContext: UserContext; }): Promise; createCode: ( input: ( @@ -181,7 +182,7 @@ export declare type RecipeInterface = { ) & { userInputCode?: string; tenantId: string; - userContext: any; + userContext: UserContext; } ) => Promise<{ status: "OK"; @@ -197,7 +198,7 @@ export declare type RecipeInterface = { deviceId: string; userInputCode?: string; tenantId: string; - userContext: any; + userContext: UserContext; }) => Promise< | { status: "OK"; @@ -220,13 +221,15 @@ export declare type RecipeInterface = { deviceId: string; preAuthSessionId: string; tenantId: string; - userContext: any; + shouldAttemptAccountLinkingIfAllowed?: boolean; + userContext: UserContext; } | { linkCode: string; preAuthSessionId: string; tenantId: string; - userContext: any; + shouldAttemptAccountLinkingIfAllowed?: boolean; + userContext: UserContext; } ) => Promise< | { @@ -234,7 +237,6 @@ export declare type RecipeInterface = { createdNewRecipeUser: boolean; user: User; recipeUserId: RecipeUserId; - isValidFirstFactorForTenant: boolean | undefined; } | { status: "INCORRECT_USER_INPUT_CODE_ERROR" | "EXPIRED_USER_INPUT_CODE_ERROR"; @@ -249,7 +251,7 @@ export declare type RecipeInterface = { recipeUserId: RecipeUserId; email?: string | null; phoneNumber?: string | null; - userContext: any; + userContext: UserContext; }) => Promise< | { status: @@ -268,12 +270,12 @@ export declare type RecipeInterface = { | { email: string; tenantId: string; - userContext: any; + userContext: UserContext; } | { phoneNumber: string; tenantId: string; - userContext: any; + userContext: UserContext; } ) => Promise<{ status: "OK"; @@ -281,25 +283,25 @@ export declare type RecipeInterface = { revokeCode: (input: { codeId: string; tenantId: string; - userContext: any; + userContext: UserContext; }) => Promise<{ status: "OK"; }>; - listCodesByEmail: (input: { email: string; tenantId: string; userContext: any }) => Promise; + listCodesByEmail: (input: { email: string; tenantId: string; userContext: UserContext }) => Promise; listCodesByPhoneNumber: (input: { phoneNumber: string; tenantId: string; - userContext: any; + userContext: UserContext; }) => Promise; listCodesByDeviceId: (input: { deviceId: string; tenantId: string; - userContext: any; + userContext: UserContext; }) => Promise; listCodesByPreAuthSessionId: (input: { preAuthSessionId: string; tenantId: string; - userContext: any; + userContext: UserContext; }) => Promise; }; export declare type PasswordlessAPIOptions = PasswordlessAPIOptionsOriginal; @@ -312,7 +314,7 @@ export declare type APIInterface = { redirectURIOnProviderDashboard: string; tenantId: string; options: ThirdPartyAPIOptions; - userContext: any; + userContext: UserContext; }) => Promise< | { status: "OK"; @@ -328,7 +330,7 @@ export declare type APIInterface = { provider: TypeProvider; tenantId: string; options: ThirdPartyAPIOptions; - userContext: any; + userContext: UserContext; } & ( | { redirectURIInfo: { @@ -368,6 +370,7 @@ export declare type APIInterface = { status: "SIGN_IN_UP_NOT_ALLOWED"; reason: string; } + | MFAFlowErrors | GeneralErrorResponse >); appleRedirectHandlerPOST: @@ -375,7 +378,7 @@ export declare type APIInterface = { | ((input: { formPostInfoFromProvider: any; options: ThirdPartyAPIOptions; - userContext: any; + userContext: UserContext; }) => Promise); createCodePOST: | undefined @@ -390,7 +393,7 @@ export declare type APIInterface = { ) & { tenantId: string; options: PasswordlessAPIOptions; - userContext: any; + userContext: UserContext; } ) => Promise< | { @@ -414,7 +417,7 @@ export declare type APIInterface = { } & { tenantId: string; options: PasswordlessAPIOptions; - userContext: any; + userContext: UserContext; } ) => Promise< | GeneralErrorResponse @@ -438,7 +441,7 @@ export declare type APIInterface = { ) & { tenantId: string; options: PasswordlessAPIOptions; - userContext: any; + userContext: UserContext; } ) => Promise< | { @@ -452,7 +455,6 @@ export declare type APIInterface = { failedCodeInputAttemptCount: number; maximumCodeInputAttempts: number; } - | GeneralErrorResponse | { status: "RESTART_FLOW_ERROR"; } @@ -460,6 +462,8 @@ export declare type APIInterface = { status: "SIGN_IN_UP_NOT_ALLOWED"; reason: string; } + | GeneralErrorResponse + | MFAFlowErrors >); passwordlessUserEmailExistsGET: | undefined @@ -467,7 +471,7 @@ export declare type APIInterface = { email: string; tenantId: string; options: PasswordlessAPIOptions; - userContext: any; + userContext: UserContext; }) => Promise< | { status: "OK"; @@ -481,7 +485,7 @@ export declare type APIInterface = { phoneNumber: string; tenantId: string; options: PasswordlessAPIOptions; - userContext: any; + userContext: UserContext; }) => Promise< | { status: "OK"; diff --git a/lib/build/recipe/totp/api/createDevice.d.ts b/lib/build/recipe/totp/api/createDevice.d.ts index 03e962c55..aaa135298 100644 --- a/lib/build/recipe/totp/api/createDevice.d.ts +++ b/lib/build/recipe/totp/api/createDevice.d.ts @@ -1,7 +1,8 @@ // @ts-nocheck import { APIInterface, APIOptions } from ".."; +import { UserContext } from "../../../types"; export default function createDeviceAPI( apiImplementation: APIInterface, options: APIOptions, - userContext: any + userContext: UserContext ): Promise; diff --git a/lib/build/recipe/totp/api/implementation.js b/lib/build/recipe/totp/api/implementation.js index bc25e14c9..d964d6d1b 100644 --- a/lib/build/recipe/totp/api/implementation.js +++ b/lib/build/recipe/totp/api/implementation.js @@ -7,6 +7,7 @@ var __importDefault = Object.defineProperty(exports, "__esModule", { value: true }); const recipe_1 = __importDefault(require("../recipe")); const error_1 = __importDefault(require("../../session/error")); +const recipe_2 = __importDefault(require("../../multifactorauth/recipe")); function getAPIInterface() { return { createDevicePOST: async function ({ deviceName, options, session, userContext }) { @@ -50,23 +51,75 @@ function getAPIInterface() { verifyDevicePOST: async function ({ deviceName, totp, options, session, userContext }) { const userId = session.getUserId(); const tenantId = session.getTenantId(); - return await options.recipeImplementation.verifyDevice({ + const mfaInstance = recipe_2.default.getInstance(); + if (mfaInstance === undefined) { + throw new Error("should never come here"); // TOTP can't work without MFA + } + const validateMfaRes = await mfaInstance.validateForMultifactorAuthBeforeFactorCompletion({ + req: options.req, + res: options.res, + tenantId, + factorIdInProgress: "totp", + session, + userLoggingIn: undefined, + isAlreadySetup: false, + userContext, + }); + if (validateMfaRes.status === "DISALLOWED_FIRST_FACTOR_ERROR") { + throw new Error("Should never come here"); // TOTP is never a first factor + } + if (validateMfaRes.status !== "OK") { + return validateMfaRes; + } + const res = await options.recipeImplementation.verifyDevice({ tenantId, userId, deviceName, totp, userContext, }); + if (res.status === "OK") { + const sessionRes = await mfaInstance.createOrUpdateSessionForMultifactorAuthAfterFactorCompletion({ + req: options.req, + res: options.res, + tenantId, + factorIdInProgress: "totp", + isAlreadySetup: false, + userContext, + }); + if (sessionRes.status != "OK") { + return sessionRes; + } + } + return res; }, verifyTOTPPOST: async function ({ totp, options, session, userContext }) { const userId = session.getUserId(); const tenantId = session.getTenantId(); - return await options.recipeImplementation.verifyTOTP({ + const mfaInstance = recipe_2.default.getInstance(); + if (mfaInstance === undefined) { + throw new Error("should never come here"); // TOTP can't work without MFA + } + const res = await options.recipeImplementation.verifyTOTP({ tenantId, userId, totp, userContext, }); + if (res.status === "OK") { + const sessionRes = await mfaInstance.createOrUpdateSessionForMultifactorAuthAfterFactorCompletion({ + req: options.req, + res: options.res, + tenantId, + factorIdInProgress: "totp", + isAlreadySetup: true, + userContext, + }); + if (sessionRes.status != "OK") { + return sessionRes; + } + } + return res; }, }; } diff --git a/lib/build/recipe/totp/api/listDevices.d.ts b/lib/build/recipe/totp/api/listDevices.d.ts index 5ceacdbd2..d6ba65d69 100644 --- a/lib/build/recipe/totp/api/listDevices.d.ts +++ b/lib/build/recipe/totp/api/listDevices.d.ts @@ -1,7 +1,8 @@ // @ts-nocheck import { APIInterface, APIOptions } from ".."; +import { UserContext } from "../../../types"; export default function listDevicesAPI( apiImplementation: APIInterface, options: APIOptions, - userContext: any + userContext: UserContext ): Promise; diff --git a/lib/build/recipe/totp/api/removeDevice.d.ts b/lib/build/recipe/totp/api/removeDevice.d.ts index 4690adc2a..047515bef 100644 --- a/lib/build/recipe/totp/api/removeDevice.d.ts +++ b/lib/build/recipe/totp/api/removeDevice.d.ts @@ -1,7 +1,8 @@ // @ts-nocheck import { APIInterface, APIOptions } from ".."; +import { UserContext } from "../../../types"; export default function removeDeviceAPI( apiImplementation: APIInterface, options: APIOptions, - userContext: any + userContext: UserContext ): Promise; diff --git a/lib/build/recipe/totp/api/verifyDevice.d.ts b/lib/build/recipe/totp/api/verifyDevice.d.ts index 67215dbb4..d4de4bcc9 100644 --- a/lib/build/recipe/totp/api/verifyDevice.d.ts +++ b/lib/build/recipe/totp/api/verifyDevice.d.ts @@ -1,7 +1,8 @@ // @ts-nocheck import { APIInterface, APIOptions } from ".."; +import { UserContext } from "../../../types"; export default function verifyDeviceAPI( apiImplementation: APIInterface, options: APIOptions, - userContext: any + userContext: UserContext ): Promise; diff --git a/lib/build/recipe/totp/api/verifyTOTP.d.ts b/lib/build/recipe/totp/api/verifyTOTP.d.ts index 7c7e6fae2..925a9f40a 100644 --- a/lib/build/recipe/totp/api/verifyTOTP.d.ts +++ b/lib/build/recipe/totp/api/verifyTOTP.d.ts @@ -1,7 +1,8 @@ // @ts-nocheck import { APIInterface, APIOptions } from ".."; +import { UserContext } from "../../../types"; export default function verifyTOTPAPI( apiImplementation: APIInterface, options: APIOptions, - userContext: any + userContext: UserContext ): Promise; diff --git a/lib/build/recipe/totp/index.d.ts b/lib/build/recipe/totp/index.d.ts index 060440af7..0af9a2b36 100644 --- a/lib/build/recipe/totp/index.d.ts +++ b/lib/build/recipe/totp/index.d.ts @@ -8,7 +8,7 @@ export default class Wrapper { deviceName?: string, skew?: number, period?: number, - userContext?: any + userContext?: Record ): Promise< | { status: "OK"; @@ -22,7 +22,7 @@ export default class Wrapper { >; static listDevices( userId: string, - userContext?: any + userContext?: Record ): Promise<{ status: "OK"; devices: { @@ -36,14 +36,14 @@ export default class Wrapper { userId: string, existingDeviceName: string, newDeviceName: string, - userContext?: any + userContext?: Record ): Promise<{ status: "OK" | "DEVICE_ALREADY_EXISTS_ERROR" | "UNKNOWN_DEVICE_ERROR"; }>; static removeDevice( userId: string, deviceName: string, - userContext?: any + userContext?: Record ): Promise<{ status: "OK"; didDeviceExist: boolean; @@ -53,7 +53,7 @@ export default class Wrapper { userId: string, deviceName: string, totp: string, - userContext?: any + userContext?: Record ): Promise< | { status: "OK"; @@ -76,7 +76,7 @@ export default class Wrapper { tenantId: string, userId: string, totp: string, - userContext?: any + userContext?: Record ): Promise< | { status: "OK" | "UNKNOWN_USER_ID_ERROR"; diff --git a/lib/build/recipe/totp/index.js b/lib/build/recipe/totp/index.js index 3f300218e..d3bfcfe4c 100644 --- a/lib/build/recipe/totp/index.js +++ b/lib/build/recipe/totp/index.js @@ -20,6 +20,7 @@ var __importDefault = }; Object.defineProperty(exports, "__esModule", { value: true }); exports.verifyTOTP = exports.verifyDevice = exports.removeDevice = exports.updateDevice = exports.listDevices = exports.createDevice = exports.init = void 0; +const utils_1 = require("../../utils"); const recipe_1 = __importDefault(require("./recipe")); class Wrapper { static async createDevice(userId, deviceName, skew, period, userContext) { @@ -28,13 +29,13 @@ class Wrapper { deviceName, skew, period, - userContext: userContext !== null && userContext !== void 0 ? userContext : {}, + userContext: utils_1.getUserContext(userContext), }); } static async listDevices(userId, userContext) { return recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.listDevices({ userId, - userContext: userContext !== null && userContext !== void 0 ? userContext : {}, + userContext: utils_1.getUserContext(userContext), }); } static async updateDevice(userId, existingDeviceName, newDeviceName, userContext) { @@ -42,14 +43,14 @@ class Wrapper { userId, existingDeviceName, newDeviceName, - userContext: userContext !== null && userContext !== void 0 ? userContext : {}, + userContext: utils_1.getUserContext(userContext), }); } static async removeDevice(userId, deviceName, userContext) { return recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.removeDevice({ userId, deviceName, - userContext: userContext !== null && userContext !== void 0 ? userContext : {}, + userContext: utils_1.getUserContext(userContext), }); } static async verifyDevice(tenantId, userId, deviceName, totp, userContext) { @@ -58,7 +59,7 @@ class Wrapper { userId, deviceName, totp, - userContext: userContext !== null && userContext !== void 0 ? userContext : {}, + userContext: utils_1.getUserContext(userContext), }); } static async verifyTOTP(tenantId, userId, totp, userContext) { @@ -66,7 +67,7 @@ class Wrapper { tenantId, userId, totp, - userContext: userContext !== null && userContext !== void 0 ? userContext : {}, + userContext: utils_1.getUserContext(userContext), }); } } diff --git a/lib/build/recipe/totp/recipe.d.ts b/lib/build/recipe/totp/recipe.d.ts index 8b0e16b63..7a0d69677 100644 --- a/lib/build/recipe/totp/recipe.d.ts +++ b/lib/build/recipe/totp/recipe.d.ts @@ -3,7 +3,7 @@ import { BaseRequest, BaseResponse } from "../../framework"; import NormalisedURLPath from "../../normalisedURLPath"; import RecipeModule from "../../recipeModule"; import STError from "../../error"; -import { APIHandled, HTTPMethod, NormalisedAppinfo, RecipeListFunction } from "../../types"; +import { APIHandled, HTTPMethod, NormalisedAppinfo, RecipeListFunction, UserContext } from "../../types"; import { APIInterface, GetUserIdentifierInfoForUserIdFunc, @@ -31,7 +31,7 @@ export default class Recipe extends RecipeModule { res: BaseResponse, _: NormalisedURLPath, __: HTTPMethod, - userContext: any + userContext: UserContext ) => Promise; handleError: (err: STError, _: BaseRequest, __: BaseResponse) => Promise; getAllCORSHeaders: () => string[]; diff --git a/lib/build/recipe/totp/recipe.js b/lib/build/recipe/totp/recipe.js index 802ceff2e..05a1b83cd 100644 --- a/lib/build/recipe/totp/recipe.js +++ b/lib/build/recipe/totp/recipe.js @@ -34,6 +34,8 @@ const verifyTOTP_1 = __importDefault(require("./api/verifyTOTP")); const listDevices_1 = __importDefault(require("./api/listDevices")); const removeDevice_1 = __importDefault(require("./api/removeDevice")); const __1 = require("../.."); +const postSuperTokensInitCallbacks_1 = require("../../postSuperTokensInitCallbacks"); +const recipe_1 = __importDefault(require("../multifactorauth/recipe")); class Recipe extends recipeModule_1.default { constructor(recipeId, appInfo, isInServerlessEnv, config) { super(recipeId, appInfo); @@ -167,6 +169,42 @@ class Recipe extends recipeModule_1.default { return (appInfo, isInServerlessEnv) => { if (Recipe.instance === undefined) { Recipe.instance = new Recipe(Recipe.RECIPE_ID, appInfo, isInServerlessEnv, config); + postSuperTokensInitCallbacks_1.PostSuperTokensInitCallbacks.addPostInitCallback(() => { + const mfaInstance = recipe_1.default.getInstance(); + if (mfaInstance !== undefined) { + mfaInstance.addGetAllFactorsFromOtherRecipesFunc((tenantConfig) => { + if (tenantConfig.totp.enabled === false) { + return { + factorIds: [], + firstFactorIds: [], + }; + } + return { + factorIds: ["totp"], + firstFactorIds: [], + }; + }); + mfaInstance.addGetFactorsSetupForUserFromOtherRecipes( + async (user, tenantConfig, userContext) => { + if (tenantConfig.totp.enabled === false) { + return []; + } + const deviceRes = await Recipe.getInstanceOrThrowError().recipeInterfaceImpl.listDevices( + { + userId: user.id, + userContext, + } + ); + for (const device of deviceRes.devices) { + if (device.verified) { + return ["totp"]; + } + } + return []; + } + ); + } + }); return Recipe.instance; } else { throw new Error("TOTP recipe has already been initialised. Please check your code for bugs."); diff --git a/lib/build/recipe/totp/recipeImplementation.js b/lib/build/recipe/totp/recipeImplementation.js index 77d65d527..1e18e0024 100644 --- a/lib/build/recipe/totp/recipeImplementation.js +++ b/lib/build/recipe/totp/recipeImplementation.js @@ -17,7 +17,6 @@ function getRecipeInterface(querier, config) { deviceName: input.deviceName, skew: (_a = input.skew) !== null && _a !== void 0 ? _a : config.defaultSkew, period: (_b = input.period) !== null && _b !== void 0 ? _b : config.defaultPeriod, - userContext: input.userContext, }, input.userContext ); @@ -38,7 +37,6 @@ function getRecipeInterface(querier, config) { userId: input.userId, existingDeviceName: input.existingDeviceName, newDeviceName: input.newDeviceName, - userContext: input.userContext, }, input.userContext ); @@ -48,7 +46,6 @@ function getRecipeInterface(querier, config) { new normalisedURLPath_1.default("/recipe/totp/device/list"), { userId: input.userId, - userContext: input.userContext, }, input.userContext ); @@ -59,7 +56,6 @@ function getRecipeInterface(querier, config) { { userId: input.userId, deviceName: input.deviceName, - userContext: input.userContext, }, input.userContext ); @@ -71,7 +67,6 @@ function getRecipeInterface(querier, config) { userId: input.userId, deviceName: input.deviceName, totp: input.totp, - userContext: input.userContext, }, input.userContext ); @@ -82,7 +77,6 @@ function getRecipeInterface(querier, config) { { userId: input.userId, totp: input.totp, - userContext: input.userContext, }, input.userContext ); diff --git a/lib/build/recipe/totp/types.d.ts b/lib/build/recipe/totp/types.d.ts index 84b4da763..2299511e5 100644 --- a/lib/build/recipe/totp/types.d.ts +++ b/lib/build/recipe/totp/types.d.ts @@ -1,11 +1,12 @@ // @ts-nocheck import { BaseRequest, BaseResponse } from "../../framework"; import OverrideableBuilder from "supertokens-js-override"; -import { GeneralErrorResponse } from "../../types"; +import { GeneralErrorResponse, UserContext } from "../../types"; import { SessionContainerInterface } from "../session/types"; +import { MFAFlowErrors } from "../multifactorauth/types"; export declare type GetUserIdentifierInfoForUserIdFunc = ( userId: string, - userContext: any + userContext: UserContext ) => Promise< | { status: "OK"; @@ -48,7 +49,7 @@ export declare type RecipeInterface = { deviceName?: string; skew?: number; period?: number; - userContext: any; + userContext: UserContext; }) => Promise< | { status: "OK"; @@ -64,13 +65,13 @@ export declare type RecipeInterface = { userId: string; existingDeviceName: string; newDeviceName: string; - userContext: any; + userContext: UserContext; }) => Promise<{ status: "OK" | "UNKNOWN_DEVICE_ERROR" | "DEVICE_ALREADY_EXISTS_ERROR"; }>; listDevices: (input: { userId: string; - userContext: any; + userContext: UserContext; }) => Promise<{ status: "OK"; devices: { @@ -83,7 +84,7 @@ export declare type RecipeInterface = { removeDevice: (input: { userId: string; deviceName: string; - userContext: any; + userContext: UserContext; }) => Promise<{ status: "OK"; didDeviceExist: boolean; @@ -93,7 +94,7 @@ export declare type RecipeInterface = { userId: string; deviceName: string; totp: string; - userContext: string; + userContext: UserContext; }) => Promise< | { status: "OK"; @@ -116,7 +117,7 @@ export declare type RecipeInterface = { tenantId: string; userId: string; totp: string; - userContext: any; + userContext: UserContext; }) => Promise< | { status: "OK" | "UNKNOWN_USER_ID_ERROR"; @@ -145,7 +146,7 @@ export declare type APIInterface = { deviceName?: string; options: APIOptions; session: SessionContainerInterface; - userContext: any; + userContext: UserContext; }) => Promise< | { status: "OK" | "DEVICE_ALREADY_EXISTS_ERROR"; @@ -161,7 +162,7 @@ export declare type APIInterface = { listDevicesGET: (input: { options: APIOptions; session: SessionContainerInterface; - userContext: any; + userContext: UserContext; }) => Promise< | { status: "OK"; @@ -178,7 +179,7 @@ export declare type APIInterface = { deviceName: string; options: APIOptions; session: SessionContainerInterface; - userContext: any; + userContext: UserContext; }) => Promise< | { status: "OK"; @@ -191,7 +192,7 @@ export declare type APIInterface = { totp: string; options: APIOptions; session: SessionContainerInterface; - userContext: any; + userContext: UserContext; }) => Promise< | { status: "OK"; @@ -209,13 +210,17 @@ export declare type APIInterface = { status: "LIMIT_REACHED_ERROR"; retryAfterMs: number; } + | { + status: "FACTOR_SETUP_NOT_ALLOWED_ERROR"; + } + | MFAFlowErrors | GeneralErrorResponse >; verifyTOTPPOST: (input: { totp: string; options: APIOptions; session: SessionContainerInterface; - userContext: any; + userContext: UserContext; }) => Promise< | { status: "OK" | "UNKNOWN_USER_ID_ERROR"; @@ -229,6 +234,7 @@ export declare type APIInterface = { status: "LIMIT_REACHED_ERROR"; retryAfterMs: number; } + | MFAFlowErrors | GeneralErrorResponse >; }; diff --git a/lib/build/recipe/usermetadata/index.d.ts b/lib/build/recipe/usermetadata/index.d.ts index 91f6865fa..17987da81 100644 --- a/lib/build/recipe/usermetadata/index.d.ts +++ b/lib/build/recipe/usermetadata/index.d.ts @@ -6,7 +6,7 @@ export default class Wrapper { static init: typeof Recipe.init; static getUserMetadata( userId: string, - userContext?: any + userContext?: Record ): Promise<{ status: "OK"; metadata: any; @@ -14,14 +14,14 @@ export default class Wrapper { static updateUserMetadata( userId: string, metadataUpdate: JSONObject, - userContext?: any + userContext?: Record ): Promise<{ status: "OK"; metadata: JSONObject; }>; static clearUserMetadata( userId: string, - userContext?: any + userContext?: Record ): Promise<{ status: "OK"; }>; diff --git a/lib/build/recipe/usermetadata/index.js b/lib/build/recipe/usermetadata/index.js index e6be0a417..b0884bab7 100644 --- a/lib/build/recipe/usermetadata/index.js +++ b/lib/build/recipe/usermetadata/index.js @@ -20,25 +20,26 @@ var __importDefault = }; Object.defineProperty(exports, "__esModule", { value: true }); exports.clearUserMetadata = exports.updateUserMetadata = exports.getUserMetadata = exports.init = void 0; +const utils_1 = require("../../utils"); const recipe_1 = __importDefault(require("./recipe")); class Wrapper { static async getUserMetadata(userId, userContext) { return await recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.getUserMetadata({ userId, - userContext: userContext === undefined ? {} : userContext, + userContext: utils_1.getUserContext(userContext), }); } static async updateUserMetadata(userId, metadataUpdate, userContext) { return await recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.updateUserMetadata({ userId, metadataUpdate, - userContext: userContext === undefined ? {} : userContext, + userContext: utils_1.getUserContext(userContext), }); } static async clearUserMetadata(userId, userContext) { return await recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.clearUserMetadata({ userId, - userContext: userContext === undefined ? {} : userContext, + userContext: utils_1.getUserContext(userContext), }); } } diff --git a/lib/build/recipe/usermetadata/recipeImplementation.js b/lib/build/recipe/usermetadata/recipeImplementation.js index 51689ee59..6963da82d 100644 --- a/lib/build/recipe/usermetadata/recipeImplementation.js +++ b/lib/build/recipe/usermetadata/recipeImplementation.js @@ -13,32 +13,50 @@ * License for the specific language governing permissions and limitations * under the License. */ -var __importDefault = (this && this.__importDefault) || function (mod) { - return (mod && mod.__esModule) ? mod : { "default": mod }; -}; +var __importDefault = + (this && this.__importDefault) || + function (mod) { + return mod && mod.__esModule ? mod : { default: mod }; + }; Object.defineProperty(exports, "__esModule", { value: true }); const normalisedURLPath_1 = __importDefault(require("../../normalisedURLPath")); function getRecipeInterface(querier) { return { getUserMetadata: function ({ userId, userContext }) { - return querier.sendGetRequest(new normalisedURLPath_1.default("/recipe/user/metadata"), { userId }, userContext); + return querier.sendGetRequest( + new normalisedURLPath_1.default("/recipe/user/metadata"), + { userId }, + userContext + ); }, updateUserMetadata: function ({ userId, metadataUpdate, userContext }) { - return querier.sendPutRequest(new normalisedURLPath_1.default("/recipe/user/metadata"), { - userId, - metadataUpdate, - }, userContext); + return querier.sendPutRequest( + new normalisedURLPath_1.default("/recipe/user/metadata"), + { + userId, + metadataUpdate, + }, + userContext + ); }, updateUserMetadataInternal: function ({ userId, metadataUpdate, userContext }) { - return querier.sendPutRequest(new normalisedURLPath_1.default("/recipe/user/metadata"), { - userId, - metadataUpdate, - }, userContext); + return querier.sendPutRequest( + new normalisedURLPath_1.default("/recipe/user/metadata"), + { + userId, + metadataUpdate, + }, + userContext + ); }, clearUserMetadata: function ({ userId, userContext }) { - return querier.sendPostRequest(new normalisedURLPath_1.default("/recipe/user/metadata/remove"), { - userId, - }, userContext); + return querier.sendPostRequest( + new normalisedURLPath_1.default("/recipe/user/metadata/remove"), + { + userId, + }, + userContext + ); }, }; } diff --git a/lib/build/recipe/usermetadata/types.d.ts b/lib/build/recipe/usermetadata/types.d.ts index 97ce164e0..37d0e2d00 100644 --- a/lib/build/recipe/usermetadata/types.d.ts +++ b/lib/build/recipe/usermetadata/types.d.ts @@ -1,6 +1,6 @@ // @ts-nocheck import OverrideableBuilder from "supertokens-js-override"; -import { JSONObject } from "../../types"; +import { JSONObject, UserContext } from "../../types"; export declare type TypeInput = { override?: { functions?: ( @@ -23,7 +23,7 @@ export declare type APIInterface = {}; export declare type RecipeInterface = { getUserMetadata: (input: { userId: string; - userContext: any; + userContext: UserContext; }) => Promise<{ status: "OK"; metadata: any; @@ -39,7 +39,7 @@ export declare type RecipeInterface = { updateUserMetadata: (input: { userId: string; metadataUpdate: JSONObject; - userContext: any; + userContext: UserContext; }) => Promise<{ status: "OK"; metadata: JSONObject; @@ -47,14 +47,14 @@ export declare type RecipeInterface = { updateUserMetadataInternal: (input: { userId: string; metadataUpdate: JSONObject; - userContext: any; + userContext: UserContext; }) => Promise<{ status: "OK"; metadata: JSONObject; }>; clearUserMetadata: (input: { userId: string; - userContext: any; + userContext: UserContext; }) => Promise<{ status: "OK"; }>; diff --git a/lib/build/recipe/userroles/index.d.ts b/lib/build/recipe/userroles/index.d.ts index 350905e5d..11a803d66 100644 --- a/lib/build/recipe/userroles/index.d.ts +++ b/lib/build/recipe/userroles/index.d.ts @@ -9,7 +9,7 @@ export default class Wrapper { tenantId: string, userId: string, role: string, - userContext?: any + userContext?: Record ): Promise< | { status: "OK"; @@ -23,7 +23,7 @@ export default class Wrapper { tenantId: string, userId: string, role: string, - userContext?: any + userContext?: Record ): Promise< | { status: "OK"; @@ -36,7 +36,7 @@ export default class Wrapper { static getRolesForUser( tenantId: string, userId: string, - userContext?: any + userContext?: Record ): Promise<{ status: "OK"; roles: string[]; @@ -44,7 +44,7 @@ export default class Wrapper { static getUsersThatHaveRole( tenantId: string, role: string, - userContext?: any + userContext?: Record ): Promise< | { status: "OK"; @@ -57,14 +57,14 @@ export default class Wrapper { static createNewRoleOrAddPermissions( role: string, permissions: string[], - userContext?: any + userContext?: Record ): Promise<{ status: "OK"; createdNewRole: boolean; }>; static getPermissionsForRole( role: string, - userContext?: any + userContext?: Record ): Promise< | { status: "OK"; @@ -77,26 +77,26 @@ export default class Wrapper { static removePermissionsFromRole( role: string, permissions: string[], - userContext?: any + userContext?: Record ): Promise<{ status: "OK" | "UNKNOWN_ROLE_ERROR"; }>; static getRolesThatHavePermission( permission: string, - userContext?: any + userContext?: Record ): Promise<{ status: "OK"; roles: string[]; }>; static deleteRole( role: string, - userContext?: any + userContext?: Record ): Promise<{ status: "OK"; didRoleExist: boolean; }>; static getAllRoles( - userContext?: any + userContext?: Record ): Promise<{ status: "OK"; roles: string[]; diff --git a/lib/build/recipe/userroles/index.js b/lib/build/recipe/userroles/index.js index b19da7fe8..21e01ff10 100644 --- a/lib/build/recipe/userroles/index.js +++ b/lib/build/recipe/userroles/index.js @@ -20,6 +20,7 @@ var __importDefault = }; Object.defineProperty(exports, "__esModule", { value: true }); exports.PermissionClaim = exports.UserRoleClaim = exports.getAllRoles = exports.deleteRole = exports.getRolesThatHavePermission = exports.removePermissionsFromRole = exports.getPermissionsForRole = exports.createNewRoleOrAddPermissions = exports.getUsersThatHaveRole = exports.getRolesForUser = exports.removeUserRole = exports.addRoleToUser = exports.init = void 0; +const utils_1 = require("../../utils"); const permissionClaim_1 = require("./permissionClaim"); const recipe_1 = __importDefault(require("./recipe")); const userRoleClaim_1 = require("./userRoleClaim"); @@ -29,7 +30,7 @@ class Wrapper { userId, role, tenantId, - userContext: userContext === undefined ? {} : userContext, + userContext: utils_1.getUserContext(userContext), }); } static async removeUserRole(tenantId, userId, role, userContext) { @@ -37,58 +38,58 @@ class Wrapper { userId, role, tenantId, - userContext: userContext === undefined ? {} : userContext, + userContext: utils_1.getUserContext(userContext), }); } static async getRolesForUser(tenantId, userId, userContext) { return await recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.getRolesForUser({ userId, tenantId, - userContext: userContext === undefined ? {} : userContext, + userContext: utils_1.getUserContext(userContext), }); } static async getUsersThatHaveRole(tenantId, role, userContext) { return await recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.getUsersThatHaveRole({ role, tenantId, - userContext: userContext === undefined ? {} : userContext, + userContext: utils_1.getUserContext(userContext), }); } static async createNewRoleOrAddPermissions(role, permissions, userContext) { return await recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.createNewRoleOrAddPermissions({ role, permissions, - userContext: userContext === undefined ? {} : userContext, + userContext: utils_1.getUserContext(userContext), }); } static async getPermissionsForRole(role, userContext) { return await recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.getPermissionsForRole({ role, - userContext: userContext === undefined ? {} : userContext, + userContext: utils_1.getUserContext(userContext), }); } static async removePermissionsFromRole(role, permissions, userContext) { return await recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.removePermissionsFromRole({ role, permissions, - userContext: userContext === undefined ? {} : userContext, + userContext: utils_1.getUserContext(userContext), }); } static async getRolesThatHavePermission(permission, userContext) { return await recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.getRolesThatHavePermission({ permission, - userContext: userContext === undefined ? {} : userContext, + userContext: utils_1.getUserContext(userContext), }); } static async deleteRole(role, userContext) { return await recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.deleteRole({ role, - userContext: userContext === undefined ? {} : userContext, + userContext: utils_1.getUserContext(userContext), }); } static async getAllRoles(userContext) { return await recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.getAllRoles({ - userContext: userContext === undefined ? {} : userContext, + userContext: utils_1.getUserContext(userContext), }); } } diff --git a/lib/build/recipe/userroles/permissionClaim.js b/lib/build/recipe/userroles/permissionClaim.js index 6ee522b54..c022b4080 100644 --- a/lib/build/recipe/userroles/permissionClaim.js +++ b/lib/build/recipe/userroles/permissionClaim.js @@ -15,7 +15,7 @@ class PermissionClaimClass extends primitiveArrayClaim_1.PrimitiveArrayClaim { constructor() { super({ key: "st-perm", - async fetchValue(userId, _recipeUserId, tenantId, userContext) { + async fetchValue(userId, _recipeUserId, tenantId, _currentPayload, userContext) { const recipe = recipe_1.default.getInstanceOrThrowError(); // We fetch the roles because the rolesClaim may not be present in the payload const userRoles = await recipe.recipeInterfaceImpl.getRolesForUser({ diff --git a/lib/build/recipe/userroles/recipeImplementation.js b/lib/build/recipe/userroles/recipeImplementation.js index f9e762eaf..ca12893f9 100644 --- a/lib/build/recipe/userroles/recipeImplementation.js +++ b/lib/build/recipe/userroles/recipeImplementation.js @@ -97,7 +97,7 @@ function getRecipeInterface(querier) { userContext ); }, - getAllRoles: function (userContext) { + getAllRoles: function ({ userContext }) { return querier.sendGetRequest(new normalisedURLPath_1.default("/recipe/roles"), {}, userContext); }, }; diff --git a/lib/build/recipe/userroles/types.d.ts b/lib/build/recipe/userroles/types.d.ts index b65813bf9..c79ca9b8d 100644 --- a/lib/build/recipe/userroles/types.d.ts +++ b/lib/build/recipe/userroles/types.d.ts @@ -1,5 +1,6 @@ // @ts-nocheck import OverrideableBuilder from "supertokens-js-override"; +import { UserContext } from "../../types"; export declare type TypeInput = { skipAddingRolesToAccessToken?: boolean; skipAddingPermissionsToAccessToken?: boolean; @@ -28,7 +29,7 @@ export declare type RecipeInterface = { userId: string; role: string; tenantId: string; - userContext: any; + userContext: UserContext; }) => Promise< | { status: "OK"; @@ -42,7 +43,7 @@ export declare type RecipeInterface = { userId: string; role: string; tenantId: string; - userContext: any; + userContext: UserContext; }) => Promise< | { status: "OK"; @@ -55,7 +56,7 @@ export declare type RecipeInterface = { getRolesForUser: (input: { userId: string; tenantId: string; - userContext: any; + userContext: UserContext; }) => Promise<{ status: "OK"; roles: string[]; @@ -63,7 +64,7 @@ export declare type RecipeInterface = { getUsersThatHaveRole: (input: { role: string; tenantId: string; - userContext: any; + userContext: UserContext; }) => Promise< | { status: "OK"; @@ -76,14 +77,14 @@ export declare type RecipeInterface = { createNewRoleOrAddPermissions: (input: { role: string; permissions: string[]; - userContext: any; + userContext: UserContext; }) => Promise<{ status: "OK"; createdNewRole: boolean; }>; getPermissionsForRole: (input: { role: string; - userContext: any; + userContext: UserContext; }) => Promise< | { status: "OK"; @@ -96,26 +97,26 @@ export declare type RecipeInterface = { removePermissionsFromRole: (input: { role: string; permissions: string[]; - userContext: any; + userContext: UserContext; }) => Promise<{ status: "OK" | "UNKNOWN_ROLE_ERROR"; }>; getRolesThatHavePermission: (input: { permission: string; - userContext: any; + userContext: UserContext; }) => Promise<{ status: "OK"; roles: string[]; }>; deleteRole: (input: { role: string; - userContext: any; + userContext: UserContext; }) => Promise<{ status: "OK"; didRoleExist: boolean; }>; getAllRoles: (input: { - userContext: any; + userContext: UserContext; }) => Promise<{ status: "OK"; roles: string[]; diff --git a/lib/build/recipe/userroles/userRoleClaim.js b/lib/build/recipe/userroles/userRoleClaim.js index 6415692a1..ec8bdb305 100644 --- a/lib/build/recipe/userroles/userRoleClaim.js +++ b/lib/build/recipe/userroles/userRoleClaim.js @@ -15,7 +15,7 @@ class UserRoleClaimClass extends primitiveArrayClaim_1.PrimitiveArrayClaim { constructor() { super({ key: "st-role", - async fetchValue(userId, _recipeUserId, tenantId, userContext) { + async fetchValue(userId, _recipeUserId, tenantId, _currentPayload, userContext) { const recipe = recipe_1.default.getInstanceOrThrowError(); const res = await recipe.recipeInterfaceImpl.getRolesForUser({ userId, diff --git a/lib/build/recipeModule.d.ts b/lib/build/recipeModule.d.ts index 44c840f74..778cd14ee 100644 --- a/lib/build/recipeModule.d.ts +++ b/lib/build/recipeModule.d.ts @@ -1,6 +1,6 @@ // @ts-nocheck import STError from "./error"; -import { NormalisedAppinfo, APIHandled, HTTPMethod } from "./types"; +import { NormalisedAppinfo, APIHandled, HTTPMethod, UserContext } from "./types"; import NormalisedURLPath from "./normalisedURLPath"; import { BaseRequest, BaseResponse } from "./framework"; export default abstract class RecipeModule { @@ -12,7 +12,7 @@ export default abstract class RecipeModule { returnAPIIdIfCanHandleRequest: ( path: NormalisedURLPath, method: HTTPMethod, - userContext: any + userContext: UserContext ) => Promise< | { id: string; @@ -28,9 +28,14 @@ export default abstract class RecipeModule { response: BaseResponse, path: NormalisedURLPath, method: HTTPMethod, - userContext: any + userContext: UserContext ): Promise; - abstract handleError(error: STError, request: BaseRequest, response: BaseResponse, userContext: any): Promise; + abstract handleError( + error: STError, + request: BaseRequest, + response: BaseResponse, + userContext: UserContext + ): Promise; abstract getAllCORSHeaders(): string[]; abstract isErrorFromThisRecipe(err: any): err is STError; } diff --git a/lib/build/supertokens.d.ts b/lib/build/supertokens.d.ts index 4845b8412..e4f7f6777 100644 --- a/lib/build/supertokens.d.ts +++ b/lib/build/supertokens.d.ts @@ -1,5 +1,5 @@ // @ts-nocheck -import { TypeInput, NormalisedAppinfo, HTTPMethod, SuperTokensInfo } from "./types"; +import { TypeInput, NormalisedAppinfo, HTTPMethod, SuperTokensInfo, UserContext } from "./types"; import RecipeModule from "./recipeModule"; import NormalisedURLPath from "./normalisedURLPath"; import type { BaseRequest, BaseResponse } from "./framework"; @@ -24,20 +24,20 @@ export default class SuperTokens { response: BaseResponse, path: NormalisedURLPath, method: HTTPMethod, - userContext: any + userContext: UserContext ) => Promise; getAllCORSHeaders: () => string[]; getUserCount: ( - includeRecipeIds?: string[] | undefined, - tenantId?: string | undefined, - userContext?: any + includeRecipeIds: string[] | undefined, + tenantId: string | undefined, + userContext: UserContext ) => Promise; createUserIdMapping: (input: { superTokensUserId: string; externalUserId: string; externalUserIdInfo?: string; force?: boolean; - userContext?: any; + userContext: UserContext; }) => Promise< | { status: "OK" | "UNKNOWN_SUPERTOKENS_USER_ID_ERROR"; @@ -51,7 +51,7 @@ export default class SuperTokens { getUserIdMapping: (input: { userId: string; userIdType?: "SUPERTOKENS" | "EXTERNAL" | "ANY"; - userContext?: any; + userContext: UserContext; }) => Promise< | { status: "OK"; @@ -67,7 +67,7 @@ export default class SuperTokens { userId: string; userIdType?: "SUPERTOKENS" | "EXTERNAL" | "ANY"; force?: boolean; - userContext?: any; + userContext: UserContext; }) => Promise<{ status: "OK"; didMappingExist: boolean; @@ -76,11 +76,11 @@ export default class SuperTokens { userId: string; userIdType?: "SUPERTOKENS" | "EXTERNAL" | "ANY"; externalUserIdInfo?: string; - userContext?: any; + userContext: UserContext; }) => Promise<{ status: "OK" | "UNKNOWN_MAPPING_ERROR"; }>; - middleware: (request: BaseRequest, response: BaseResponse, userContext: any) => Promise; - errorHandler: (err: any, request: BaseRequest, response: BaseResponse, userContext: any) => Promise; - getRequestFromUserContext: (userContext: any | undefined) => BaseRequest | undefined; + middleware: (request: BaseRequest, response: BaseResponse, userContext: UserContext) => Promise; + errorHandler: (err: any, request: BaseRequest, response: BaseResponse, userContext: UserContext) => Promise; + getRequestFromUserContext: (userContext: UserContext | undefined) => BaseRequest | undefined; } diff --git a/lib/build/supertokens.js b/lib/build/supertokens.js index 509064c4c..ec9ef2375 100644 --- a/lib/build/supertokens.js +++ b/lib/build/supertokens.js @@ -66,7 +66,7 @@ class SuperTokens { includeRecipeIds: includeRecipeIdsStr, includeAllTenants: tenantId === undefined, }, - userContext === undefined ? {} : userContext + userContext ); return Number(response.count); }; @@ -83,7 +83,7 @@ class SuperTokens { externalUserIdInfo: input.externalUserIdInfo, force: input.force, }, - input.userContext === undefined ? {} : input.userContext + input.userContext ); } else { throw new global.Error("Please upgrade the SuperTokens core to >= 3.15.0"); @@ -100,7 +100,7 @@ class SuperTokens { userId: input.userId, userIdType: input.userIdType, }, - input.userContext === undefined ? {} : input.userContext + input.userContext ); return response; } else { @@ -118,7 +118,7 @@ class SuperTokens { userIdType: input.userIdType, force: input.force, }, - input.userContext === undefined ? {} : input.userContext + input.userContext ); } else { throw new global.Error("Please upgrade the SuperTokens core to >= 3.15.0"); @@ -135,7 +135,7 @@ class SuperTokens { userIdType: input.userIdType, externalUserIdInfo: input.externalUserIdInfo, }, - input.userContext === undefined ? {} : input.userContext + input.userContext ); } else { throw new global.Error("Please upgrade the SuperTokens core to >= 3.15.0"); @@ -318,6 +318,7 @@ class SuperTokens { } this.isInServerlessEnv = config.isInServerlessEnv === undefined ? false : config.isInServerlessEnv; let multitenancyFound = false; + let totpFound = false; let userMetadataFound = false; let multiFactorAuthFound = false; // Multitenancy recipe is an always initialized recipe and needs to be imported this way @@ -325,7 +326,8 @@ class SuperTokens { // between `supertokens.ts` -> `recipeModule.ts` -> `multitenancy/recipe.ts` let MultitenancyRecipe = require("./recipe/multitenancy/recipe").default; let UserMetadataRecipe = require("./recipe/usermetadata/recipe").default; - let MultiFactorAuthRecipe = require("./recipe/multiFactorAuth/recipe").default; + let MultiFactorAuthRecipe = require("./recipe/multifactorauth/recipe").default; + let TotpRecipe = require("./recipe/totp/recipe").default; this.recipeModules = config.recipeList.map((func) => { const recipeModule = func(this.appInfo, this.isInServerlessEnv); if (recipeModule.getRecipeId() === MultitenancyRecipe.RECIPE_ID) { @@ -334,12 +336,19 @@ class SuperTokens { userMetadataFound = true; } else if (recipeModule.getRecipeId() === MultiFactorAuthRecipe.RECIPE_ID) { multiFactorAuthFound = true; + } else if (recipeModule.getRecipeId() === TotpRecipe.RECIPE_ID) { + totpFound = true; } return recipeModule; }); if (!multitenancyFound) { this.recipeModules.push(MultitenancyRecipe.init()(this.appInfo, this.isInServerlessEnv)); } + if (totpFound && !multiFactorAuthFound) { + // we want MFA to be enabled if totp is enabled + this.recipeModules.push(MultiFactorAuthRecipe.init()(this.appInfo, this.isInServerlessEnv)); + multiFactorAuthFound = true; + } if (multiFactorAuthFound && !userMetadataFound) { // we want user metadata to be initialized if MFA is enabled this.recipeModules.push(UserMetadataRecipe.init()(this.appInfo, this.isInServerlessEnv)); diff --git a/lib/build/types.d.ts b/lib/build/types.d.ts index 6b3e0e34b..1edba3b3f 100644 --- a/lib/build/types.d.ts +++ b/lib/build/types.d.ts @@ -5,10 +5,16 @@ import NormalisedURLPath from "./normalisedURLPath"; import { TypeFramework } from "./framework/types"; import { RecipeLevelUser } from "./recipe/accountlinking/types"; import { BaseRequest } from "./framework"; +declare const __brand: unique symbol; +declare type Brand = { + [__brand]: B; +}; +declare type Branded = T & Brand; +export declare type UserContext = Branded, "UserContext">; export declare type AppInfo = { appName: string; websiteDomain?: string; - origin?: string | ((input: { request: BaseRequest | undefined; userContext: any }) => string); + origin?: string | ((input: { request: BaseRequest | undefined; userContext: UserContext }) => string); websiteBasePath?: string; apiDomain: string; apiBasePath?: string; @@ -16,10 +22,10 @@ export declare type AppInfo = { }; export declare type NormalisedAppinfo = { appName: string; - getOrigin: (input: { request: BaseRequest | undefined; userContext: any }) => NormalisedURLDomain; + getOrigin: (input: { request: BaseRequest | undefined; userContext: UserContext }) => NormalisedURLDomain; apiDomain: NormalisedURLDomain; topLevelAPIDomain: string; - getTopLevelWebsiteDomain: (input: { request: BaseRequest | undefined; userContext: any }) => string; + getTopLevelWebsiteDomain: (input: { request: BaseRequest | undefined; userContext: UserContext }) => string; apiBasePath: NormalisedURLPath; apiGatewayPath: NormalisedURLPath; websiteBasePath: NormalisedURLPath; @@ -38,7 +44,7 @@ export declare type TypeInput = { isInServerlessEnv?: boolean; debug?: boolean; }; -export declare type NetworkInterceptor = (request: HttpRequest, userContext: any) => HttpRequest; +export declare type NetworkInterceptor = (request: HttpRequest, userContext: UserContext) => HttpRequest; export interface HttpRequest { url: string; method: HTTPMethod; @@ -86,3 +92,4 @@ export declare type User = { })[]; toJson: () => any; }; +export {}; diff --git a/lib/build/utils.d.ts b/lib/build/utils.d.ts index b7b2b17ed..7f9a76a75 100644 --- a/lib/build/utils.d.ts +++ b/lib/build/utils.d.ts @@ -1,5 +1,5 @@ // @ts-nocheck -import type { AppInfo, NormalisedAppinfo, HTTPMethod, JSONObject } from "./types"; +import type { AppInfo, NormalisedAppinfo, HTTPMethod, JSONObject, UserContext } from "./types"; import type { BaseRequest, BaseResponse } from "./framework"; import { User } from "./user"; import { SessionContainer } from "./recipe/session"; @@ -24,8 +24,12 @@ export declare function doesRequestSupportFDI(req: BaseRequest, version: string) export declare function getRidFromHeader(req: BaseRequest): string | undefined; export declare function frontendHasInterceptor(req: BaseRequest): boolean; export declare function humaniseMilliseconds(ms: number): string; -export declare function makeDefaultUserContextFromAPI(request: BaseRequest): any; -export declare function setRequestInUserContextIfNotDefined(userContext: any | undefined, request: BaseRequest): any; +export declare function makeDefaultUserContextFromAPI(request: BaseRequest): UserContext; +export declare function getUserContext(inputUserContext?: Record): UserContext; +export declare function setRequestInUserContextIfNotDefined( + userContext: UserContext | undefined, + request: BaseRequest +): UserContext; export declare function getTopLevelDomainForSameSiteResolution(url: string): string; export declare function getFromObjectCaseInsensitive(key: string, object: Record): T | undefined; export declare function postWithFetch( diff --git a/lib/build/utils.js b/lib/build/utils.js index 5a57b061f..0a93fb4ca 100644 --- a/lib/build/utils.js +++ b/lib/build/utils.js @@ -41,7 +41,7 @@ var __importDefault = return mod && mod.__esModule ? mod : { default: mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); -exports.normaliseEmail = exports.postWithFetch = exports.getFromObjectCaseInsensitive = exports.getTopLevelDomainForSameSiteResolution = exports.setRequestInUserContextIfNotDefined = exports.makeDefaultUserContextFromAPI = exports.humaniseMilliseconds = exports.frontendHasInterceptor = exports.getRidFromHeader = exports.doesRequestSupportFDI = exports.getBackwardsCompatibleUserInfo = exports.isAnIpAddress = exports.send200Response = exports.sendNon200Response = exports.sendNon200ResponseWithMessage = exports.normaliseHttpMethod = exports.normaliseInputAppInfoOrThrowError = exports.maxVersion = exports.getLargestVersionFromIntersection = exports.doFetch = void 0; +exports.normaliseEmail = exports.postWithFetch = exports.getFromObjectCaseInsensitive = exports.getTopLevelDomainForSameSiteResolution = exports.setRequestInUserContextIfNotDefined = exports.getUserContext = exports.makeDefaultUserContextFromAPI = exports.humaniseMilliseconds = exports.frontendHasInterceptor = exports.getRidFromHeader = exports.doesRequestSupportFDI = exports.getBackwardsCompatibleUserInfo = exports.isAnIpAddress = exports.send200Response = exports.sendNon200Response = exports.sendNon200ResponseWithMessage = exports.normaliseHttpMethod = exports.normaliseInputAppInfoOrThrowError = exports.maxVersion = exports.getLargestVersionFromIntersection = exports.doFetch = void 0; const psl = __importStar(require("psl")); const normalisedURLDomain_1 = __importDefault(require("./normalisedURLDomain")); const normalisedURLPath_1 = __importDefault(require("./normalisedURLPath")); @@ -283,6 +283,10 @@ function makeDefaultUserContextFromAPI(request) { return setRequestInUserContextIfNotDefined({}, request); } exports.makeDefaultUserContextFromAPI = makeDefaultUserContextFromAPI; +function getUserContext(inputUserContext) { + return inputUserContext !== null && inputUserContext !== void 0 ? inputUserContext : {}; +} +exports.getUserContext = getUserContext; function setRequestInUserContextIfNotDefined(userContext, request) { if (userContext === undefined) { userContext = {}; diff --git a/lib/build/version.js b/lib/build/version.js index 1f3e8eafb..1d6c9b7a7 100644 --- a/lib/build/version.js +++ b/lib/build/version.js @@ -16,6 +16,6 @@ exports.dashboardVersion = exports.cdiSupported = exports.version = void 0; * under the License. */ exports.version = "16.6.5"; -exports.cdiSupported = ["4.1"]; +exports.cdiSupported = ["5.0"]; // Note: The actual script import for dashboard uses v{DASHBOARD_VERSION} exports.dashboardVersion = "0.9"; diff --git a/lib/ts/framework/utils.ts b/lib/ts/framework/utils.ts index 12da0cbbe..b9fdd7095 100644 --- a/lib/ts/framework/utils.ts +++ b/lib/ts/framework/utils.ts @@ -258,7 +258,9 @@ export function setHeaderForExpressLikeResponse(res: Response, key: string, valu } } } catch (err) { - throw new Error("Error while setting header with key: " + key + " and value: " + value); + throw new Error( + "Error while setting header with key: " + key + " and value: " + value + "\nError: " + (err.message ?? err) + ); } } diff --git a/lib/ts/index.ts b/lib/ts/index.ts index 9a1795923..a6b3d323e 100644 --- a/lib/ts/index.ts +++ b/lib/ts/index.ts @@ -15,11 +15,12 @@ import SuperTokens from "./supertokens"; import SuperTokensError from "./error"; -import { User as UserType } from "./types"; +import { UserContext, User as UserType } from "./types"; import AccountLinking from "./recipe/accountlinking/recipe"; import { AccountInfo } from "./recipe/accountlinking/types"; import RecipeUserId from "./recipeUserId"; import { User } from "./user"; +import { getUserContext } from "./utils"; // For Express export default class SuperTokensWrapper { @@ -33,8 +34,12 @@ export default class SuperTokensWrapper { return SuperTokens.getInstanceOrThrowError().getAllCORSHeaders(); } - static getUserCount(includeRecipeIds?: string[], tenantId?: string, userContext?: any) { - return SuperTokens.getInstanceOrThrowError().getUserCount(includeRecipeIds, tenantId, userContext); + static getUserCount(includeRecipeIds?: string[], tenantId?: string, userContext?: Record) { + return SuperTokens.getInstanceOrThrowError().getUserCount( + includeRecipeIds, + tenantId, + getUserContext(userContext) + ); } static getUsersOldestFirst(input: { @@ -43,7 +48,7 @@ export default class SuperTokensWrapper { paginationToken?: string; includeRecipeIds?: string[]; query?: { [key: string]: string }; - userContext?: any; + userContext?: Record; }): Promise<{ users: UserType[]; nextPaginationToken?: string; @@ -51,7 +56,7 @@ export default class SuperTokensWrapper { return AccountLinking.getInstance().recipeInterfaceImpl.getUsers({ timeJoinedOrder: "ASC", ...input, - userContext: input.userContext === undefined ? {} : input.userContext, + userContext: getUserContext(input.userContext), }); } @@ -61,7 +66,7 @@ export default class SuperTokensWrapper { paginationToken?: string; includeRecipeIds?: string[]; query?: { [key: string]: string }; - userContext?: any; + userContext?: Record; }): Promise<{ users: UserType[]; nextPaginationToken?: string; @@ -69,7 +74,7 @@ export default class SuperTokensWrapper { return AccountLinking.getInstance().recipeInterfaceImpl.getUsers({ timeJoinedOrder: "DESC", ...input, - userContext: input.userContext === undefined ? {} : input.userContext, + userContext: getUserContext(input.userContext), }); } @@ -78,41 +83,53 @@ export default class SuperTokensWrapper { externalUserId: string; externalUserIdInfo?: string; force?: boolean; - userContext?: any; + userContext?: Record; }) { - return SuperTokens.getInstanceOrThrowError().createUserIdMapping(input); + return SuperTokens.getInstanceOrThrowError().createUserIdMapping({ + ...input, + userContext: getUserContext(input.userContext), + }); } static getUserIdMapping(input: { userId: string; userIdType?: "SUPERTOKENS" | "EXTERNAL" | "ANY"; - userContext?: any; + userContext?: Record; }) { - return SuperTokens.getInstanceOrThrowError().getUserIdMapping(input); + return SuperTokens.getInstanceOrThrowError().getUserIdMapping({ + ...input, + userContext: getUserContext(input.userContext), + }); } static deleteUserIdMapping(input: { userId: string; userIdType?: "SUPERTOKENS" | "EXTERNAL" | "ANY"; force?: boolean; - userContext?: any; + userContext?: Record; }) { - return SuperTokens.getInstanceOrThrowError().deleteUserIdMapping(input); + return SuperTokens.getInstanceOrThrowError().deleteUserIdMapping({ + ...input, + userContext: getUserContext(input.userContext), + }); } static updateOrDeleteUserIdMappingInfo(input: { userId: string; userIdType?: "SUPERTOKENS" | "EXTERNAL" | "ANY"; externalUserIdInfo?: string; - userContext?: any; + userContext?: Record; }) { - return SuperTokens.getInstanceOrThrowError().updateOrDeleteUserIdMappingInfo(input); + return SuperTokens.getInstanceOrThrowError().updateOrDeleteUserIdMappingInfo({ + ...input, + userContext: getUserContext(input.userContext), + }); } - static async getUser(userId: string, userContext?: any) { + static async getUser(userId: string, userContext?: Record) { return await AccountLinking.getInstance().recipeInterfaceImpl.getUser({ userId, - userContext: userContext === undefined ? {} : userContext, + userContext: getUserContext(userContext), }); } @@ -120,27 +137,31 @@ export default class SuperTokensWrapper { tenantId: string, accountInfo: AccountInfo, doUnionOfAccountInfo: boolean = false, - userContext?: any + userContext?: Record ) { return await AccountLinking.getInstance().recipeInterfaceImpl.listUsersByAccountInfo({ tenantId, accountInfo, doUnionOfAccountInfo, - userContext: userContext === undefined ? {} : userContext, + userContext: getUserContext(userContext), }); } - static async deleteUser(userId: string, removeAllLinkedAccounts: boolean = true, userContext?: any) { + static async deleteUser( + userId: string, + removeAllLinkedAccounts: boolean = true, + userContext?: Record + ) { return await AccountLinking.getInstance().recipeInterfaceImpl.deleteUser({ userId, removeAllLinkedAccounts, - userContext: userContext === undefined ? {} : userContext, + userContext: getUserContext(userContext), }); } static convertToRecipeUserId(recipeUserId: string): RecipeUserId { return new RecipeUserId(recipeUserId); } - static getRequestFromUserContext(userContext: any | undefined) { + static getRequestFromUserContext(userContext: UserContext | undefined) { return SuperTokens.getInstanceOrThrowError().getRequestFromUserContext(userContext); } } diff --git a/lib/ts/ingredients/emaildelivery/services/smtp.ts b/lib/ts/ingredients/emaildelivery/services/smtp.ts index f35fc2aeb..0df07df0d 100644 --- a/lib/ts/ingredients/emaildelivery/services/smtp.ts +++ b/lib/ts/ingredients/emaildelivery/services/smtp.ts @@ -13,6 +13,7 @@ * under the License. */ import OverrideableBuilder from "supertokens-js-override"; +import { UserContext } from "../../../types"; export interface SMTPServiceConfig { host: string; @@ -33,11 +34,11 @@ export interface GetContentResult { toEmail: string; } -export type TypeInputSendRawEmail = GetContentResult & { userContext: any }; +export type TypeInputSendRawEmail = GetContentResult & { userContext: UserContext }; export type ServiceInterface = { sendRawEmail: (input: TypeInputSendRawEmail) => Promise; - getContent: (input: T & { userContext: any }) => Promise; + getContent: (input: T & { userContext: UserContext }) => Promise; }; export type TypeInput = { diff --git a/lib/ts/ingredients/emaildelivery/types.ts b/lib/ts/ingredients/emaildelivery/types.ts index ac7aa6da9..e3dcdb26e 100644 --- a/lib/ts/ingredients/emaildelivery/types.ts +++ b/lib/ts/ingredients/emaildelivery/types.ts @@ -13,9 +13,10 @@ * under the License. */ import OverrideableBuilder from "supertokens-js-override"; +import { UserContext } from "../../types"; export type EmailDeliveryInterface = { - sendEmail: (input: T & { tenantId: string; userContext: any }) => Promise; + sendEmail: (input: T & { tenantId: string; userContext: UserContext }) => Promise; }; /** diff --git a/lib/ts/ingredients/smsdelivery/services/twilio.ts b/lib/ts/ingredients/smsdelivery/services/twilio.ts index 5ef866db3..58963c19d 100644 --- a/lib/ts/ingredients/smsdelivery/services/twilio.ts +++ b/lib/ts/ingredients/smsdelivery/services/twilio.ts @@ -14,6 +14,7 @@ */ import OverrideableBuilder from "supertokens-js-override"; import { ClientOpts } from "twilio/lib/base/BaseTwilio"; +import { UserContext } from "../../../types"; /** * only one of "from" and "messagingServiceSid" should be passed. @@ -42,7 +43,7 @@ export interface GetContentResult { toPhoneNumber: string; } -export type TypeInputSendRawSms = GetContentResult & { userContext: any } & ( +export type TypeInputSendRawSms = GetContentResult & { userContext: UserContext } & ( | { from: string; } @@ -53,7 +54,7 @@ export type TypeInputSendRawSms = GetContentResult & { userContext: any } & ( export type ServiceInterface = { sendRawSms: (input: TypeInputSendRawSms) => Promise; - getContent: (input: T & { userContext: any }) => Promise; + getContent: (input: T & { userContext: UserContext }) => Promise; }; export type TypeInput = { diff --git a/lib/ts/ingredients/smsdelivery/types.ts b/lib/ts/ingredients/smsdelivery/types.ts index 72fe5acae..3dfe3727d 100644 --- a/lib/ts/ingredients/smsdelivery/types.ts +++ b/lib/ts/ingredients/smsdelivery/types.ts @@ -13,9 +13,10 @@ * under the License. */ import OverrideableBuilder from "supertokens-js-override"; +import { UserContext } from "../../types"; export type SmsDeliveryInterface = { - sendSms: (input: T & { tenantId: string; userContext: any }) => Promise; + sendSms: (input: T & { tenantId: string; userContext: UserContext }) => Promise; }; /** diff --git a/lib/ts/querier.ts b/lib/ts/querier.ts index 023863cb3..baa3cc8b5 100644 --- a/lib/ts/querier.ts +++ b/lib/ts/querier.ts @@ -18,8 +18,9 @@ import NormalisedURLDomain from "./normalisedURLDomain"; import NormalisedURLPath from "./normalisedURLPath"; import { PROCESS_STATE, ProcessState } from "./processState"; import { RATE_LIMIT_STATUS_CODE } from "./constants"; -import { NetworkInterceptor } from "./types"; import { logDebugMessage } from "./logger"; +import { UserContext } from "./types"; +import { NetworkInterceptor } from "./types"; export class Querier { private static initCalled = false; @@ -118,7 +119,9 @@ export class Querier { } // path should start with "/" - sendPostRequest = async (path: NormalisedURLPath, body: any, userContext: any): Promise => { + sendPostRequest = async (path: NormalisedURLPath, body: any, userContext: UserContext): Promise => { + this.invalidateCoreCallCache(userContext); + const { body: respBody } = await this.sendRequestHelper( path, "POST", @@ -168,7 +171,14 @@ export class Querier { }; // path should start with "/" - sendDeleteRequest = async (path: NormalisedURLPath, body: any, params: any, userContext: any): Promise => { + sendDeleteRequest = async ( + path: NormalisedURLPath, + body: any, + params: any | undefined, + userContext: UserContext + ): Promise => { + this.invalidateCoreCallCache(userContext); + const { body: respBody } = await this.sendRequestHelper( path, "DELETE", @@ -227,8 +237,20 @@ export class Querier { sendGetRequest = async ( path: NormalisedURLPath, params: Record, - userContext: any + userContext: UserContext ): Promise => { + const sortedKeys = Object.keys(params).sort(); + let uniqueKey = path.getAsStringDangerous(); + + for (const key of sortedKeys) { + const value = params[key]; + uniqueKey += `;${key}=${value}`; + } + + if (uniqueKey in (userContext._default?.coreCallCache ?? {})) { + return userContext._default.coreCallCache[uniqueKey]; + } + const { body: respBody } = await this.sendRequestHelper( path, "GET", @@ -275,13 +297,21 @@ export class Querier { }, this.__hosts?.length || 0 ); + + userContext._default = { + ...userContext._default, + coreCallCache: { + ...userContext._default?.coreCallCache, + [uniqueKey]: respBody, + }, + }; return respBody; }; sendGetRequestWithResponseHeaders = async ( path: NormalisedURLPath, params: Record, - userContext: any + userContext: UserContext ): Promise<{ body: any; headers: Headers }> => { return await this.sendRequestHelper( path, @@ -332,7 +362,9 @@ export class Querier { }; // path should start with "/" - sendPutRequest = async (path: NormalisedURLPath, body: any, userContext: any): Promise => { + sendPutRequest = async (path: NormalisedURLPath, body: any, userContext: UserContext): Promise => { + this.invalidateCoreCallCache(userContext); + const { body: respBody } = await this.sendRequestHelper( path, "PUT", @@ -379,6 +411,13 @@ export class Querier { return respBody; }; + invalidateCoreCallCache = (userContext: UserContext) => { + userContext._default = { + ...userContext._default, + coreCallCache: {}, + }; + }; + public getAllCoreUrlsForPath(path: string) { if (this.__hosts === undefined) { return []; @@ -427,6 +466,7 @@ export class Querier { Querier.lastTriedIndex = Querier.lastTriedIndex % this.__hosts.length; try { ProcessState.getInstance().addState(PROCESS_STATE.CALLING_SERVICE_IN_REQUEST_HELPER); + logDebugMessage(`core-call: ${method} ${url}`); let response = await requestFunc(url); if (process.env.TEST_MODE === "testing") { Querier.hostsAliveForTesting.add(currentDomain + currentBasePath); diff --git a/lib/ts/recipe/accountlinking/index.ts b/lib/ts/recipe/accountlinking/index.ts index 30da994c5..7fbbdcf4b 100644 --- a/lib/ts/recipe/accountlinking/index.ts +++ b/lib/ts/recipe/accountlinking/index.ts @@ -17,6 +17,7 @@ import Recipe from "./recipe"; import type { RecipeInterface, AccountInfoWithRecipeId } from "./types"; import RecipeUserId from "../../recipeUserId"; import { getUser } from "../.."; +import { getUserContext } from "../../utils"; export default class Wrapper { static init = Recipe.init; @@ -33,7 +34,7 @@ export default class Wrapper { static async createPrimaryUserIdOrLinkAccounts( tenantId: string, recipeUserId: RecipeUserId, - userContext: any = {} + userContext?: Record ) { const user = await getUser(recipeUserId.getAsString(), userContext); if (user === undefined) { @@ -44,7 +45,7 @@ export default class Wrapper { return await Recipe.getInstance().createPrimaryUserIdOrLinkAccounts({ tenantId, user, - userContext, + userContext: getUserContext(userContext), }); } @@ -60,7 +61,7 @@ export default class Wrapper { static async getPrimaryUserThatCanBeLinkedToRecipeUserId( tenantId: string, recipeUserId: RecipeUserId, - userContext: any = {} + userContext?: Record ) { const user = await getUser(recipeUserId.getAsString(), userContext); if (user === undefined) { @@ -70,44 +71,44 @@ export default class Wrapper { return await Recipe.getInstance().getPrimaryUserThatCanBeLinkedToRecipeUserId({ tenantId, user, - userContext, + userContext: getUserContext(userContext), }); } - static async canCreatePrimaryUser(recipeUserId: RecipeUserId, userContext: any = {}) { + static async canCreatePrimaryUser(recipeUserId: RecipeUserId, userContext?: Record) { return await Recipe.getInstance().recipeInterfaceImpl.canCreatePrimaryUser({ recipeUserId, - userContext, + userContext: getUserContext(userContext), }); } - static async createPrimaryUser(recipeUserId: RecipeUserId, userContext: any = {}) { + static async createPrimaryUser(recipeUserId: RecipeUserId, userContext?: Record) { return await Recipe.getInstance().recipeInterfaceImpl.createPrimaryUser({ recipeUserId, - userContext, + userContext: getUserContext(userContext), }); } - static async canLinkAccounts(recipeUserId: RecipeUserId, primaryUserId: string, userContext: any = {}) { + static async canLinkAccounts(recipeUserId: RecipeUserId, primaryUserId: string, userContext?: Record) { return await Recipe.getInstance().recipeInterfaceImpl.canLinkAccounts({ recipeUserId, primaryUserId, - userContext, + userContext: getUserContext(userContext), }); } - static async linkAccounts(recipeUserId: RecipeUserId, primaryUserId: string, userContext: any = {}) { + static async linkAccounts(recipeUserId: RecipeUserId, primaryUserId: string, userContext?: Record) { return await Recipe.getInstance().recipeInterfaceImpl.linkAccounts({ recipeUserId, primaryUserId, - userContext, + userContext: getUserContext(userContext), }); } - static async unlinkAccount(recipeUserId: RecipeUserId, userContext: any = {}) { + static async unlinkAccount(recipeUserId: RecipeUserId, userContext?: Record) { return await Recipe.getInstance().recipeInterfaceImpl.unlinkAccount({ recipeUserId, - userContext, + userContext: getUserContext(userContext), }); } @@ -115,17 +116,17 @@ export default class Wrapper { tenantId: string, newUser: AccountInfoWithRecipeId, isVerified: boolean, - userContext?: any + userContext?: Record ) { return await Recipe.getInstance().isSignUpAllowed({ newUser, isVerified, tenantId, - userContext, + userContext: getUserContext(userContext), }); } - static async isSignInAllowed(tenantId: string, recipeUserId: RecipeUserId, userContext: any = {}) { + static async isSignInAllowed(tenantId: string, recipeUserId: RecipeUserId, userContext?: Record) { const user = await getUser(recipeUserId.getAsString(), userContext); if (user === undefined) { // Should never really come here unless a programming error happened in the app @@ -135,7 +136,7 @@ export default class Wrapper { return await Recipe.getInstance().isSignInAllowed({ user, tenantId, - userContext, + userContext: getUserContext(userContext), }); } @@ -143,7 +144,7 @@ export default class Wrapper { recipeUserId: RecipeUserId, newEmail: string, isVerified: boolean, - userContext?: any + userContext?: Record ) { const user = await getUser(recipeUserId.getAsString(), userContext); @@ -151,7 +152,7 @@ export default class Wrapper { user, newEmail, isVerified, - userContext, + userContext: getUserContext(userContext), }); } } diff --git a/lib/ts/recipe/accountlinking/recipe.ts b/lib/ts/recipe/accountlinking/recipe.ts index 52a7d9372..66aa5de01 100644 --- a/lib/ts/recipe/accountlinking/recipe.ts +++ b/lib/ts/recipe/accountlinking/recipe.ts @@ -17,18 +17,16 @@ import error from "../../error"; import type { BaseRequest, BaseResponse } from "../../framework"; import normalisedURLPath from "../../normalisedURLPath"; import RecipeModule from "../../recipeModule"; -import type { APIHandled, HTTPMethod, NormalisedAppinfo, RecipeListFunction, User } from "../../types"; +import type { APIHandled, HTTPMethod, NormalisedAppinfo, RecipeListFunction, User, UserContext } from "../../types"; import type { TypeNormalisedInput, RecipeInterface, TypeInput, AccountInfoWithRecipeId } from "./types"; -import { validateAndNormaliseUserInput } from "./utils"; +import { validateAndNormaliseUserInput, verifyEmailForRecipeUserIfLinkedAccountsAreVerified } from "./utils"; import OverrideableBuilder from "supertokens-js-override"; import RecipeImplementation from "./recipeImplementation"; import { Querier } from "../../querier"; import SuperTokensError from "../../error"; import supertokens from "../../supertokens"; -import RecipeUserId from "../../recipeUserId"; import { ProcessState, PROCESS_STATE } from "../../processState"; import { logDebugMessage } from "../../logger"; -import EmailVerificationRecipe from "../emailverification/recipe"; import { LoginMethod } from "../../user"; export default class Recipe extends RecipeModule { @@ -138,7 +136,7 @@ export default class Recipe extends RecipeModule { }: { tenantId: string; user: User; - userContext: any; + userContext: UserContext; }): Promise => { logDebugMessage("createPrimaryUserIdOrLinkAccounts called"); // TODO: fix this @@ -303,7 +301,7 @@ export default class Recipe extends RecipeModule { }: { tenantId: string; user: User; - userContext: any; + userContext: UserContext; }): Promise => { // first we check if this user itself is a // primary user or not. If it is, we return that. @@ -358,7 +356,7 @@ export default class Recipe extends RecipeModule { }: { user: User; tenantId: string; - userContext: any; + userContext: UserContext; }): Promise => { ProcessState.getInstance().addState(PROCESS_STATE.IS_SIGN_IN_ALLOWED_CALLED); @@ -384,7 +382,7 @@ export default class Recipe extends RecipeModule { newUser: AccountInfoWithRecipeId; isVerified: boolean; tenantId: string; - userContext: any; + userContext: UserContext; }): Promise => { ProcessState.getInstance().addState(PROCESS_STATE.IS_SIGN_UP_ALLOWED_CALLED); if (newUser.email !== undefined && newUser.phoneNumber !== undefined) { @@ -413,7 +411,7 @@ export default class Recipe extends RecipeModule { isVerified: boolean; tenantId: string; isSignIn: boolean; - userContext: any; + userContext: UserContext; }): Promise => { ProcessState.getInstance().addState(PROCESS_STATE.IS_SIGN_IN_UP_ALLOWED_HELPER_CALLED); // since this is a recipe level user, we have to do the following checks @@ -624,7 +622,7 @@ export default class Recipe extends RecipeModule { user?: User; newEmail: string; isVerified: boolean; - userContext: any; + userContext: UserContext; }): Promise => { /** * The purpose of this function is to check that if a recipe user ID's email @@ -729,74 +727,5 @@ export default class Recipe extends RecipeModule { return true; }; - verifyEmailForRecipeUserIfLinkedAccountsAreVerified = async (input: { - user: User; - recipeUserId: RecipeUserId; - userContext: any; - }) => { - try { - EmailVerificationRecipe.getInstanceOrThrowError(); - } catch (ignored) { - // if email verification recipe is not initialized, we do a no-op - return; - } - // This is just a helper function cause it's called in many places - // like during sign up, sign in and post linking accounts. - // This is not exposed to the developer as it's called in the relevant - // recipe functions. - // We do not do this in the core cause email verification is a different - // recipe. - // Finally, we only mark the email of this recipe user as verified and not - // the other recipe users in the primary user (if this user's email is verified), - // cause when those other users sign in, this function will be called for them anyway - if (input.user.isPrimaryUser) { - let recipeUserEmail: string | undefined = undefined; - let isAlreadyVerified = false; - input.user.loginMethods.forEach((lm) => { - if (lm.recipeUserId.getAsString() === input.recipeUserId.getAsString()) { - recipeUserEmail = lm.email; - isAlreadyVerified = lm.verified; - } - }); - - if (recipeUserEmail !== undefined) { - if (isAlreadyVerified) { - return; - } - let shouldVerifyEmail = false; - input.user.loginMethods.forEach((lm) => { - if (lm.hasSameEmailAs(recipeUserEmail) && lm.verified) { - shouldVerifyEmail = true; - } - }); - - if (shouldVerifyEmail) { - let resp = await EmailVerificationRecipe.getInstanceOrThrowError().recipeInterfaceImpl.createEmailVerificationToken( - { - // While the token we create here is tenant specific, the verification status is not - // So we can use any tenantId the user is associated with here as long as we use the - // same in the verifyEmailUsingToken call - tenantId: input.user.tenantIds[0], - recipeUserId: input.recipeUserId, - email: recipeUserEmail, - userContext: input.userContext, - } - ); - if (resp.status === "OK") { - // we purposely pass in false below cause we don't want account - // linking to happen - await EmailVerificationRecipe.getInstanceOrThrowError().recipeInterfaceImpl.verifyEmailUsingToken( - { - // See comment about tenantId in the createEmailVerificationToken params - tenantId: input.user.tenantIds[0], - token: resp.token, - attemptAccountLinking: false, - userContext: input.userContext, - } - ); - } - } - } - } - }; + verifyEmailForRecipeUserIfLinkedAccountsAreVerified = verifyEmailForRecipeUserIfLinkedAccountsAreVerified; } diff --git a/lib/ts/recipe/accountlinking/recipeImplementation.ts b/lib/ts/recipe/accountlinking/recipeImplementation.ts index 26c6a1d62..c3d80461c 100644 --- a/lib/ts/recipe/accountlinking/recipeImplementation.ts +++ b/lib/ts/recipe/accountlinking/recipeImplementation.ts @@ -19,7 +19,7 @@ import NormalisedURLPath from "../../normalisedURLPath"; import RecipeUserId from "../../recipeUserId"; import type AccountLinkingRecipe from "./recipe"; import { User } from "../../user"; -import type { User as UserType } from "../../types"; +import type { UserContext, User as UserType } from "../../types"; export default function getRecipeImplementation( querier: Querier, @@ -44,7 +44,7 @@ export default function getRecipeImplementation( paginationToken?: string; includeRecipeIds?: string[]; query?: { [key: string]: string }; - userContext: any; + userContext: UserContext; } ): Promise<{ users: UserType[]; @@ -77,7 +77,7 @@ export default function getRecipeImplementation( userContext, }: { recipeUserId: RecipeUserId; - userContext: any; + userContext: UserContext; } ): Promise< | { @@ -108,7 +108,7 @@ export default function getRecipeImplementation( userContext, }: { recipeUserId: RecipeUserId; - userContext: any; + userContext: UserContext; } ): Promise< | { @@ -148,7 +148,7 @@ export default function getRecipeImplementation( }: { recipeUserId: RecipeUserId; primaryUserId: string; - userContext: any; + userContext: UserContext; } ): Promise< | { @@ -190,7 +190,7 @@ export default function getRecipeImplementation( }: { recipeUserId: RecipeUserId; primaryUserId: string; - userContext: any; + userContext: UserContext; } ): Promise< | { @@ -268,7 +268,7 @@ export default function getRecipeImplementation( userContext, }: { recipeUserId: RecipeUserId; - userContext: any; + userContext: UserContext; } ): Promise<{ status: "OK"; @@ -285,10 +285,7 @@ export default function getRecipeImplementation( return accountsUnlinkingResult; }, - getUser: async function ( - this: RecipeInterface, - { userId, userContext }: { userId: string; userContext: any } - ): Promise { + getUser: async function (this: RecipeInterface, { userId, userContext }): Promise { let result = await querier.sendGetRequest( new NormalisedURLPath("/user/id"), { @@ -297,7 +294,8 @@ export default function getRecipeImplementation( userContext ); if (result.status === "OK") { - return new User(result.user); + const userResult = new User(result.user); + return userResult; } return undefined; }, @@ -309,7 +307,12 @@ export default function getRecipeImplementation( accountInfo, doUnionOfAccountInfo, userContext, - }: { tenantId: string; accountInfo: AccountInfo; doUnionOfAccountInfo: boolean; userContext: any } + }: { + tenantId: string; + accountInfo: AccountInfo; + doUnionOfAccountInfo: boolean; + userContext: UserContext; + } ): Promise { let result = await querier.sendGetRequest( new NormalisedURLPath(`${tenantId ?? "public"}/users/by-accountinfo`), @@ -334,7 +337,7 @@ export default function getRecipeImplementation( }: { userId: string; removeAllLinkedAccounts: boolean; - userContext: any; + userContext: UserContext; } ): Promise<{ status: "OK"; diff --git a/lib/ts/recipe/accountlinking/types.ts b/lib/ts/recipe/accountlinking/types.ts index 65d15f44f..9c5253c85 100644 --- a/lib/ts/recipe/accountlinking/types.ts +++ b/lib/ts/recipe/accountlinking/types.ts @@ -14,16 +14,16 @@ */ import OverrideableBuilder from "supertokens-js-override"; -import type { User } from "../../types"; +import type { User, UserContext } from "../../types"; import RecipeUserId from "../../recipeUserId"; export type TypeInput = { - onAccountLinked?: (user: User, newAccountInfo: RecipeLevelUser, userContext: any) => Promise; + onAccountLinked?: (user: User, newAccountInfo: RecipeLevelUser, userContext: UserContext) => Promise; shouldDoAutomaticAccountLinking?: ( newAccountInfo: AccountInfoWithRecipeId & { recipeUserId?: RecipeUserId }, user: User | undefined, tenantId: string, - userContext: any + userContext: UserContext ) => Promise< | { shouldAutomaticallyLink: false; @@ -42,12 +42,12 @@ export type TypeInput = { }; export type TypeNormalisedInput = { - onAccountLinked: (user: User, newAccountInfo: RecipeLevelUser, userContext: any) => Promise; + onAccountLinked: (user: User, newAccountInfo: RecipeLevelUser, userContext: UserContext) => Promise; shouldDoAutomaticAccountLinking: ( newAccountInfo: AccountInfoWithRecipeId & { recipeUserId?: RecipeUserId }, user: User | undefined, tenantId: string, - userContext: any + userContext: UserContext ) => Promise< | { shouldAutomaticallyLink: false; @@ -73,14 +73,14 @@ export type RecipeInterface = { paginationToken?: string; includeRecipeIds?: string[]; query?: { [key: string]: string }; - userContext: any; + userContext: UserContext; }) => Promise<{ users: User[]; nextPaginationToken?: string; }>; canCreatePrimaryUser: (input: { recipeUserId: RecipeUserId; - userContext: any; + userContext: UserContext; }) => Promise< | { status: "OK"; @@ -96,7 +96,7 @@ export type RecipeInterface = { >; createPrimaryUser: (input: { recipeUserId: RecipeUserId; - userContext: any; + userContext: UserContext; }) => Promise< | { status: "OK"; @@ -116,7 +116,7 @@ export type RecipeInterface = { canLinkAccounts: (input: { recipeUserId: RecipeUserId; primaryUserId: string; - userContext: any; + userContext: UserContext; }) => Promise< | { status: "OK"; @@ -139,7 +139,7 @@ export type RecipeInterface = { linkAccounts: (input: { recipeUserId: RecipeUserId; primaryUserId: string; - userContext: any; + userContext: UserContext; }) => Promise< | { status: "OK"; @@ -162,23 +162,23 @@ export type RecipeInterface = { >; unlinkAccount: (input: { recipeUserId: RecipeUserId; - userContext: any; + userContext: UserContext; }) => Promise<{ status: "OK"; wasRecipeUserDeleted: boolean; wasLinked: boolean; }>; - getUser: (input: { userId: string; userContext: any }) => Promise; + getUser: (input: { userId: string; userContext: UserContext }) => Promise; listUsersByAccountInfo: (input: { tenantId: string; accountInfo: AccountInfo; doUnionOfAccountInfo: boolean; - userContext: any; + userContext: UserContext; }) => Promise; deleteUser: (input: { userId: string; removeAllLinkedAccounts: boolean; - userContext: any; + userContext: UserContext; }) => Promise<{ status: "OK" }>; }; diff --git a/lib/ts/recipe/accountlinking/utils.ts b/lib/ts/recipe/accountlinking/utils.ts index c4c819563..047601b8b 100644 --- a/lib/ts/recipe/accountlinking/utils.ts +++ b/lib/ts/recipe/accountlinking/utils.ts @@ -13,8 +13,11 @@ * under the License. */ -import type { NormalisedAppinfo } from "../../types"; +import RecipeUserId from "../../recipeUserId"; +import type { NormalisedAppinfo, UserContext } from "../../types"; +import { User } from "../../user"; import type { TypeInput, RecipeInterface, TypeNormalisedInput } from "./types"; +import EmailVerificationRecipe from "../emailverification/recipe"; async function defaultOnAccountLinked() {} @@ -42,3 +45,72 @@ export function validateAndNormaliseUserInput(_: NormalisedAppinfo, config?: Typ shouldDoAutomaticAccountLinking, }; } + +export async function verifyEmailForRecipeUserIfLinkedAccountsAreVerified(input: { + user: User; + recipeUserId: RecipeUserId; + userContext: UserContext; +}) { + try { + EmailVerificationRecipe.getInstanceOrThrowError(); + } catch (ignored) { + // if email verification recipe is not initialized, we do a no-op + return; + } + // This is just a helper function cause it's called in many places + // like during sign up, sign in and post linking accounts. + // This is not exposed to the developer as it's called in the relevant + // recipe functions. + // We do not do this in the core cause email verification is a different + // recipe. + // Finally, we only mark the email of this recipe user as verified and not + // the other recipe users in the primary user (if this user's email is verified), + // cause when those other users sign in, this function will be called for them anyway + if (input.user.isPrimaryUser) { + let recipeUserEmail: string | undefined = undefined; + let isAlreadyVerified = false; + input.user.loginMethods.forEach((lm) => { + if (lm.recipeUserId.getAsString() === input.recipeUserId.getAsString()) { + recipeUserEmail = lm.email; + isAlreadyVerified = lm.verified; + } + }); + + if (recipeUserEmail !== undefined) { + if (isAlreadyVerified) { + return; + } + let shouldVerifyEmail = false; + input.user.loginMethods.forEach((lm) => { + if (lm.hasSameEmailAs(recipeUserEmail) && lm.verified) { + shouldVerifyEmail = true; + } + }); + + if (shouldVerifyEmail) { + let resp = await EmailVerificationRecipe.getInstanceOrThrowError().recipeInterfaceImpl.createEmailVerificationToken( + { + // While the token we create here is tenant specific, the verification status is not + // So we can use any tenantId the user is associated with here as long as we use the + // same in the verifyEmailUsingToken call + tenantId: input.user.tenantIds[0], + recipeUserId: input.recipeUserId, + email: recipeUserEmail, + userContext: input.userContext, + } + ); + if (resp.status === "OK") { + // we purposely pass in false below cause we don't want account + // linking to happen + await EmailVerificationRecipe.getInstanceOrThrowError().recipeInterfaceImpl.verifyEmailUsingToken({ + // See comment about tenantId in the createEmailVerificationToken params + tenantId: input.user.tenantIds[0], + token: resp.token, + attemptAccountLinking: false, + userContext: input.userContext, + }); + } + } + } + } +} diff --git a/lib/ts/recipe/dashboard/api/analytics.ts b/lib/ts/recipe/dashboard/api/analytics.ts index 76c83bbdc..c7830273d 100644 --- a/lib/ts/recipe/dashboard/api/analytics.ts +++ b/lib/ts/recipe/dashboard/api/analytics.ts @@ -20,6 +20,7 @@ import NormalisedURLPath from "../../../normalisedURLPath"; import { version as SDKVersion } from "../../../version"; import STError from "../../../error"; import { doFetch } from "../../../utils"; +import { UserContext } from "../../../types"; export type Response = { status: "OK"; @@ -29,7 +30,7 @@ export default async function analyticsPost( _: APIInterface, ___: string, options: APIOptions, - userContext: any + userContext: UserContext ): Promise { // If telemetry is disabled, dont send any event if (!SuperTokens.getInstanceOrThrowError().telemetryEnabled) { @@ -63,7 +64,7 @@ export default async function analyticsPost( telemetryId = response.telemetryId; } - numberOfUsers = await SuperTokens.getInstanceOrThrowError().getUserCount(); + numberOfUsers = await SuperTokens.getInstanceOrThrowError().getUserCount(undefined, undefined, userContext); } catch (_) { // If either telemetry id API or user count fetch fails, no event should be sent return { @@ -75,7 +76,7 @@ export default async function analyticsPost( const data = { websiteDomain: websiteDomain({ request: undefined, - userContext: {}, + userContext, }).getAsStringDangerous(), apiDomain: apiDomain.getAsStringDangerous(), appName, diff --git a/lib/ts/recipe/dashboard/api/apiKeyProtector.ts b/lib/ts/recipe/dashboard/api/apiKeyProtector.ts index bd53cd01a..ee2178b4d 100644 --- a/lib/ts/recipe/dashboard/api/apiKeyProtector.ts +++ b/lib/ts/recipe/dashboard/api/apiKeyProtector.ts @@ -12,6 +12,7 @@ * License for the specific language governing permissions and limitations * under the License. */ +import { UserContext } from "../../../types"; import RecipeError from "../error"; import { APIFunction, APIInterface, APIOptions } from "../types"; import { sendUnauthorisedAccess } from "../utils"; @@ -21,7 +22,7 @@ export default async function apiKeyProtector( tenantId: string, options: APIOptions, apiFunction: APIFunction, - userContext: any + userContext: UserContext ): Promise { let shouldAllowAccess = false; diff --git a/lib/ts/recipe/dashboard/api/dashboard.ts b/lib/ts/recipe/dashboard/api/dashboard.ts index f5f66d2db..82f484789 100644 --- a/lib/ts/recipe/dashboard/api/dashboard.ts +++ b/lib/ts/recipe/dashboard/api/dashboard.ts @@ -13,12 +13,13 @@ * under the License. */ +import { UserContext } from "../../../types"; import { APIInterface, APIOptions } from "../types"; export default async function dashboard( apiImplementation: APIInterface, options: APIOptions, - userContext: any + userContext: UserContext ): Promise { if (apiImplementation.dashboardGET === undefined) { return false; diff --git a/lib/ts/recipe/dashboard/api/listTenants.ts b/lib/ts/recipe/dashboard/api/listTenants.ts index 0383a48de..c7b3a8555 100644 --- a/lib/ts/recipe/dashboard/api/listTenants.ts +++ b/lib/ts/recipe/dashboard/api/listTenants.ts @@ -15,6 +15,7 @@ import { APIInterface, APIOptions } from "../types"; import Multitenancy from "../../multitenancy"; import { ProviderConfig } from "../../thirdparty/types"; +import { UserContext } from "../../../types"; type TenantListTenantType = { tenantId: string; @@ -39,7 +40,7 @@ export default async function listTenants( _: APIInterface, __: string, ___: APIOptions, - userContext: any + userContext: UserContext ): Promise { let tenantsRes = await Multitenancy.listAllTenants(userContext); let finalTenants: TenantListTenantType[] = []; diff --git a/lib/ts/recipe/dashboard/api/search/tagsGet.ts b/lib/ts/recipe/dashboard/api/search/tagsGet.ts index a34b9e092..8d380c109 100644 --- a/lib/ts/recipe/dashboard/api/search/tagsGet.ts +++ b/lib/ts/recipe/dashboard/api/search/tagsGet.ts @@ -16,6 +16,7 @@ import { APIInterface, APIOptions } from "../../types"; import { Querier } from "../../../../querier"; import NormalisedURLPath from "../../../../normalisedURLPath"; +import { UserContext } from "../../../../types"; type TagsResponse = { status: "OK"; tags: string[] }; @@ -23,7 +24,7 @@ export const getSearchTags = async ( _: APIInterface, ___: string, options: APIOptions, - userContext: any + userContext: UserContext ): Promise => { let querier = Querier.getNewInstanceOrThrowError(options.recipeId); let tagsResponse = await querier.sendGetRequest(new NormalisedURLPath("/user/search/tags"), {}, userContext); diff --git a/lib/ts/recipe/dashboard/api/signIn.ts b/lib/ts/recipe/dashboard/api/signIn.ts index 1a0686ba9..96df122c3 100644 --- a/lib/ts/recipe/dashboard/api/signIn.ts +++ b/lib/ts/recipe/dashboard/api/signIn.ts @@ -18,13 +18,14 @@ import { send200Response } from "../../../utils"; import STError from "../../../error"; import { Querier } from "../../../querier"; import NormalisedURLPath from "../../../normalisedURLPath"; +import { UserContext } from "../../../types"; type SignInResponse = | { status: "OK"; sessionId: string } | { status: "INVALID_CREDENTIALS_ERROR" } | { status: "USER_SUSPENDED_ERROR" }; -export default async function signIn(_: APIInterface, options: APIOptions, userContext: any): Promise { +export default async function signIn(_: APIInterface, options: APIOptions, userContext: UserContext): Promise { const { email, password } = await options.req.getJSONBody(); if (email === undefined) { diff --git a/lib/ts/recipe/dashboard/api/signOut.ts b/lib/ts/recipe/dashboard/api/signOut.ts index 3d82f9761..12128d166 100644 --- a/lib/ts/recipe/dashboard/api/signOut.ts +++ b/lib/ts/recipe/dashboard/api/signOut.ts @@ -17,12 +17,13 @@ import { APIInterface, APIOptions } from "../types"; import { send200Response } from "../../../utils"; import { Querier } from "../../../querier"; import NormalisedURLPath from "../../../normalisedURLPath"; +import { UserContext } from "../../../types"; export default async function signOut( _: APIInterface, ___: string, options: APIOptions, - userContext: any + userContext: UserContext ): Promise { if (options.config.authMode === "api-key") { send200Response(options.res, { status: "OK" }); diff --git a/lib/ts/recipe/dashboard/api/userdetails/userEmailVerifyGet.ts b/lib/ts/recipe/dashboard/api/userdetails/userEmailVerifyGet.ts index 3148ee0df..fa24c9b63 100644 --- a/lib/ts/recipe/dashboard/api/userdetails/userEmailVerifyGet.ts +++ b/lib/ts/recipe/dashboard/api/userdetails/userEmailVerifyGet.ts @@ -3,6 +3,7 @@ import STError from "../../../../error"; import EmailVerification from "../../../emailverification"; import EmailVerificationRecipe from "../../../emailverification/recipe"; import RecipeUserId from "../../../../recipeUserId"; +import { UserContext } from "../../../../types"; type Response = | { @@ -17,7 +18,7 @@ export const userEmailVerifyGet: APIFunction = async ( _: APIInterface, ___: string, options: APIOptions, - userContext: any + userContext: UserContext ): Promise => { const req = options.req; const recipeUserId = req.getKeyValueFromQuery("recipeUserId"); diff --git a/lib/ts/recipe/dashboard/api/userdetails/userEmailVerifyPut.ts b/lib/ts/recipe/dashboard/api/userdetails/userEmailVerifyPut.ts index d2e26ee4a..d5f098b16 100644 --- a/lib/ts/recipe/dashboard/api/userdetails/userEmailVerifyPut.ts +++ b/lib/ts/recipe/dashboard/api/userdetails/userEmailVerifyPut.ts @@ -2,6 +2,7 @@ import { APIInterface, APIOptions } from "../../types"; import STError from "../../../../error"; import EmailVerification from "../../../emailverification"; import RecipeUserId from "../../../../recipeUserId"; +import { UserContext } from "../../../../types"; type Response = { status: "OK"; @@ -11,7 +12,7 @@ export const userEmailVerifyPut = async ( _: APIInterface, tenantId: string, options: APIOptions, - userContext: any + userContext: UserContext ): Promise => { const requestBody = await options.req.getJSONBody(); const recipeUserId = requestBody.recipeUserId; @@ -48,6 +49,7 @@ export const userEmailVerifyPut = async ( const verifyResponse = await EmailVerification.verifyEmailUsingToken( tenantId, tokenResponse.token, + undefined, userContext ); diff --git a/lib/ts/recipe/dashboard/api/userdetails/userEmailVerifyTokenPost.ts b/lib/ts/recipe/dashboard/api/userdetails/userEmailVerifyTokenPost.ts index e7197d854..6e30aa645 100644 --- a/lib/ts/recipe/dashboard/api/userdetails/userEmailVerifyTokenPost.ts +++ b/lib/ts/recipe/dashboard/api/userdetails/userEmailVerifyTokenPost.ts @@ -2,6 +2,7 @@ import { APIInterface, APIOptions } from "../../types"; import STError from "../../../../error"; import EmailVerification from "../../../emailverification"; import { convertToRecipeUserId, getUser } from "../../../.."; +import { UserContext } from "../../../../types"; type Response = { status: "OK" | "EMAIL_ALREADY_VERIFIED_ERROR"; @@ -11,7 +12,7 @@ export const userEmailVerifyTokenPost = async ( _: APIInterface, tenantId: string, options: APIOptions, - userContext: any + userContext: UserContext ): Promise => { const requestBody = await options.req.getJSONBody(); const recipeUserId = requestBody.recipeUserId; diff --git a/lib/ts/recipe/dashboard/api/userdetails/userGet.ts b/lib/ts/recipe/dashboard/api/userdetails/userGet.ts index 60ef9fbe3..6677057f8 100644 --- a/lib/ts/recipe/dashboard/api/userdetails/userGet.ts +++ b/lib/ts/recipe/dashboard/api/userdetails/userGet.ts @@ -3,7 +3,7 @@ import STError from "../../../../error"; import UserMetaDataRecipe from "../../../usermetadata/recipe"; import UserMetaData from "../../../usermetadata"; import { getUser } from "../../../.."; -import { User } from "../../../../types"; +import { User, UserContext } from "../../../../types"; type Response = | { @@ -18,7 +18,7 @@ export const userGet: APIFunction = async ( _: APIInterface, ___: string, options: APIOptions, - userContext: any + userContext: UserContext ): Promise => { const userId = options.req.getKeyValueFromQuery("userId"); diff --git a/lib/ts/recipe/dashboard/api/userdetails/userMetadataGet.ts b/lib/ts/recipe/dashboard/api/userdetails/userMetadataGet.ts index 3ffa684bc..ad750181b 100644 --- a/lib/ts/recipe/dashboard/api/userdetails/userMetadataGet.ts +++ b/lib/ts/recipe/dashboard/api/userdetails/userMetadataGet.ts @@ -2,6 +2,7 @@ import { APIFunction, APIInterface, APIOptions } from "../../types"; import STError from "../../../../error"; import UserMetaDataRecipe from "../../../usermetadata/recipe"; import UserMetaData from "../../../usermetadata"; +import { UserContext } from "../../../../types"; type Response = | { @@ -16,7 +17,7 @@ export const userMetaDataGet: APIFunction = async ( _: APIInterface, ___: string, options: APIOptions, - userContext: any + userContext: UserContext ): Promise => { const userId = options.req.getKeyValueFromQuery("userId"); diff --git a/lib/ts/recipe/dashboard/api/userdetails/userMetadataPut.ts b/lib/ts/recipe/dashboard/api/userdetails/userMetadataPut.ts index 43c232ffe..513b305f6 100644 --- a/lib/ts/recipe/dashboard/api/userdetails/userMetadataPut.ts +++ b/lib/ts/recipe/dashboard/api/userdetails/userMetadataPut.ts @@ -2,6 +2,7 @@ import { APIInterface, APIOptions } from "../../types"; import UserMetadaRecipe from "../../../usermetadata/recipe"; import UserMetaData from "../../../usermetadata"; import STError from "../../../../error"; +import { UserContext } from "../../../../types"; type Response = { status: "OK"; @@ -11,7 +12,7 @@ export const userMetadataPut = async ( _: APIInterface, ___: string, options: APIOptions, - userContext: any + userContext: UserContext ): Promise => { const requestBody = await options.req.getJSONBody(); const userId = requestBody.userId; diff --git a/lib/ts/recipe/dashboard/api/userdetails/userPasswordPut.ts b/lib/ts/recipe/dashboard/api/userdetails/userPasswordPut.ts index fb2dae948..2a762b08a 100644 --- a/lib/ts/recipe/dashboard/api/userdetails/userPasswordPut.ts +++ b/lib/ts/recipe/dashboard/api/userdetails/userPasswordPut.ts @@ -5,6 +5,7 @@ import EmailPassword from "../../../emailpassword"; import ThirdPartyEmailPasswordRecipe from "../../../thirdpartyemailpassword/recipe"; import ThirdPartyEmailPassword from "../../../thirdpartyemailpassword"; import RecipeUserId from "../../../../recipeUserId"; +import { UserContext } from "../../../../types"; type Response = | { @@ -19,7 +20,7 @@ export const userPasswordPut = async ( _: APIInterface, tenantId: string, options: APIOptions, - userContext: any + userContext: UserContext ): Promise => { const requestBody = await options.req.getJSONBody(); const recipeUserId = requestBody.recipeUserId; diff --git a/lib/ts/recipe/dashboard/api/userdetails/userPut.ts b/lib/ts/recipe/dashboard/api/userdetails/userPut.ts index 804035251..f00eae3f6 100644 --- a/lib/ts/recipe/dashboard/api/userdetails/userPut.ts +++ b/lib/ts/recipe/dashboard/api/userdetails/userPut.ts @@ -14,6 +14,7 @@ import UserMetadata from "../../../usermetadata"; import { FORM_FIELD_EMAIL_ID } from "../../../emailpassword/constants"; import { defaultValidateEmail, defaultValidatePhoneNumber } from "../../../passwordless/utils"; import RecipeUserId from "../../../../recipeUserId"; +import { UserContext } from "../../../../types"; type Response = | { @@ -47,7 +48,7 @@ const updateEmailForRecipeId = async ( recipeUserId: RecipeUserId, email: string, tenantId: string, - userContext: any + userContext: UserContext ): Promise< | { status: "OK"; @@ -261,7 +262,7 @@ const updatePhoneForRecipeId = async ( recipeUserId: RecipeUserId, phone: string, tenantId: string, - userContext: any + userContext: UserContext ): Promise< | { status: "OK"; @@ -394,7 +395,7 @@ export const userPut = async ( _: APIInterface, tenantId: string, options: APIOptions, - userContext: any + userContext: UserContext ): Promise => { const requestBody = await options.req.getJSONBody(); const recipeUserId = requestBody.recipeUserId; @@ -453,7 +454,7 @@ export const userPut = async ( }); } - let userResponse = await getUserForRecipeId(new RecipeUserId(recipeUserId), recipeId); + let userResponse = await getUserForRecipeId(new RecipeUserId(recipeUserId), recipeId, userContext); if (userResponse.user === undefined || userResponse.recipe === undefined) { throw new Error("Should never come here"); diff --git a/lib/ts/recipe/dashboard/api/userdetails/userSessionsGet.ts b/lib/ts/recipe/dashboard/api/userdetails/userSessionsGet.ts index 44b9f88db..f043ad944 100644 --- a/lib/ts/recipe/dashboard/api/userdetails/userSessionsGet.ts +++ b/lib/ts/recipe/dashboard/api/userdetails/userSessionsGet.ts @@ -1,6 +1,7 @@ import { APIFunction, APIInterface, APIOptions } from "../../types"; import STError from "../../../../error"; import Session from "../../../session"; +import { UserContext } from "../../../../types"; type SessionType = { sessionDataInDatabase: any; @@ -20,7 +21,7 @@ export const userSessionsGet: APIFunction = async ( _: APIInterface, ___: string, options: APIOptions, - userContext: any + userContext: UserContext ): Promise => { const userId = options.req.getKeyValueFromQuery("userId"); diff --git a/lib/ts/recipe/dashboard/api/userdetails/userSessionsPost.ts b/lib/ts/recipe/dashboard/api/userdetails/userSessionsPost.ts index d7b142d2e..116b3d214 100644 --- a/lib/ts/recipe/dashboard/api/userdetails/userSessionsPost.ts +++ b/lib/ts/recipe/dashboard/api/userdetails/userSessionsPost.ts @@ -1,6 +1,7 @@ import { APIInterface, APIOptions } from "../../types"; import STError from "../../../../error"; import Session from "../../../session"; +import { UserContext } from "../../../../types"; type Response = { status: "OK"; @@ -10,7 +11,7 @@ export const userSessionsPost = async ( _: APIInterface, ___: string, options: APIOptions, - userContext: any + userContext: UserContext ): Promise => { const requestBody = await options.req.getJSONBody(); const sessionHandles = requestBody.sessionHandles; diff --git a/lib/ts/recipe/dashboard/api/userdetails/userUnlinkGet.ts b/lib/ts/recipe/dashboard/api/userdetails/userUnlinkGet.ts index f9632756e..4da025412 100644 --- a/lib/ts/recipe/dashboard/api/userdetails/userUnlinkGet.ts +++ b/lib/ts/recipe/dashboard/api/userdetails/userUnlinkGet.ts @@ -2,6 +2,7 @@ import { APIInterface, APIOptions } from "../../types"; import STError from "../../../../error"; import AccountLinking from "../../../accountlinking"; import RecipeUserId from "../../../../recipeUserId"; +import { UserContext } from "../../../../types"; type Response = { status: "OK"; @@ -11,7 +12,7 @@ export const userUnlink = async ( _: APIInterface, ___: string, options: APIOptions, - userContext: any + userContext: UserContext ): Promise => { const recipeUserId = options.req.getKeyValueFromQuery("recipeUserId"); diff --git a/lib/ts/recipe/dashboard/api/usersCountGet.ts b/lib/ts/recipe/dashboard/api/usersCountGet.ts index 5d3b30105..ca1473882 100644 --- a/lib/ts/recipe/dashboard/api/usersCountGet.ts +++ b/lib/ts/recipe/dashboard/api/usersCountGet.ts @@ -15,6 +15,7 @@ import { APIInterface, APIOptions } from "../types"; import SuperTokens from "../../../supertokens"; +import { UserContext } from "../../../types"; export type Response = { status: "OK"; @@ -25,9 +26,9 @@ export default async function usersCountGet( _: APIInterface, tenantId: string, __: APIOptions, - ___: any + userContext: UserContext ): Promise { - const count = await SuperTokens.getInstanceOrThrowError().getUserCount(undefined, tenantId); + const count = await SuperTokens.getInstanceOrThrowError().getUserCount(undefined, tenantId, userContext); return { status: "OK", diff --git a/lib/ts/recipe/dashboard/api/usersGet.ts b/lib/ts/recipe/dashboard/api/usersGet.ts index f8e91d5c3..79d2d7d25 100644 --- a/lib/ts/recipe/dashboard/api/usersGet.ts +++ b/lib/ts/recipe/dashboard/api/usersGet.ts @@ -17,6 +17,7 @@ import STError from "../../../error"; import { getUsersNewestFirst, getUsersOldestFirst } from "../../.."; import UserMetaDataRecipe from "../../usermetadata/recipe"; import UserMetaData from "../../usermetadata"; +import { UserContext } from "../../../types"; export type Response = { status: "OK"; @@ -28,7 +29,7 @@ export default async function usersGet( _: APIInterface, tenantId: string, options: APIOptions, - userContext: any + userContext: UserContext ): Promise { const req = options.req; const limit = options.req.getKeyValueFromQuery("limit"); diff --git a/lib/ts/recipe/dashboard/api/validateKey.ts b/lib/ts/recipe/dashboard/api/validateKey.ts index c4d155a58..6d0a1d38b 100644 --- a/lib/ts/recipe/dashboard/api/validateKey.ts +++ b/lib/ts/recipe/dashboard/api/validateKey.ts @@ -13,10 +13,15 @@ * under the License. */ +import { UserContext } from "../../../types"; import { APIInterface, APIOptions } from "../types"; import { sendUnauthorisedAccess, validateApiKey } from "../utils"; -export default async function validateKey(_: APIInterface, options: APIOptions, userContext: any): Promise { +export default async function validateKey( + _: APIInterface, + options: APIOptions, + userContext: UserContext +): Promise { const input = { req: options.req, config: options.config, userContext }; if (await validateApiKey(input)) { diff --git a/lib/ts/recipe/dashboard/recipe.ts b/lib/ts/recipe/dashboard/recipe.ts index 03b852f36..a6ddebacf 100644 --- a/lib/ts/recipe/dashboard/recipe.ts +++ b/lib/ts/recipe/dashboard/recipe.ts @@ -15,7 +15,7 @@ import OverrideableBuilder from "supertokens-js-override"; import RecipeModule from "../../recipeModule"; -import { APIHandled, HTTPMethod, NormalisedAppinfo, RecipeListFunction } from "../../types"; +import { APIHandled, HTTPMethod, NormalisedAppinfo, RecipeListFunction, UserContext } from "../../types"; import { APIFunction, APIInterface, APIOptions, RecipeInterface, TypeInput, TypeNormalisedInput } from "./types"; import RecipeImplementation from "./recipeImplementation"; import APIImplementation from "./api/implementation"; @@ -338,7 +338,7 @@ export default class Recipe extends RecipeModule { res: BaseResponse, __: NormalisedURLPath, ___: HTTPMethod, - userContext: any + userContext: UserContext ): Promise => { let options: APIOptions = { config: this.config, diff --git a/lib/ts/recipe/dashboard/types.ts b/lib/ts/recipe/dashboard/types.ts index 2def9f258..2c2a8ac2c 100644 --- a/lib/ts/recipe/dashboard/types.ts +++ b/lib/ts/recipe/dashboard/types.ts @@ -15,7 +15,7 @@ import OverrideableBuilder from "supertokens-js-override"; import type { BaseRequest, BaseResponse } from "../../framework"; -import { NormalisedAppinfo, User } from "../../types"; +import { NormalisedAppinfo, User, UserContext } from "../../types"; export type TypeInput = { apiKey?: string; @@ -43,8 +43,12 @@ export type TypeNormalisedInput = { }; export type RecipeInterface = { - getDashboardBundleLocation(input: { userContext: any }): Promise; - shouldAllowAccess(input: { req: BaseRequest; config: TypeNormalisedInput; userContext: any }): Promise; + getDashboardBundleLocation(input: { userContext: UserContext }): Promise; + shouldAllowAccess(input: { + req: BaseRequest; + config: TypeNormalisedInput; + userContext: UserContext; + }): Promise; }; export type APIOptions = { @@ -58,14 +62,14 @@ export type APIOptions = { }; export type APIInterface = { - dashboardGET: undefined | ((input: { options: APIOptions; userContext: any }) => Promise); + dashboardGET: undefined | ((input: { options: APIOptions; userContext: UserContext }) => Promise); }; export type APIFunction = ( apiImplementation: APIInterface, tenantId: string, options: APIOptions, - userContext: any + userContext: UserContext ) => Promise; export type RecipeIdForUser = "emailpassword" | "thirdparty" | "passwordless"; diff --git a/lib/ts/recipe/dashboard/utils.ts b/lib/ts/recipe/dashboard/utils.ts index 048294993..3a9c894bb 100644 --- a/lib/ts/recipe/dashboard/utils.ts +++ b/lib/ts/recipe/dashboard/utils.ts @@ -31,7 +31,7 @@ import PasswordlessRecipe from "../passwordless/recipe"; import ThirdPartyEmailPasswordRecipe from "../thirdpartyemailpassword/recipe"; import ThirdPartyPasswordlessRecipe from "../thirdpartypasswordless/recipe"; import RecipeUserId from "../../recipeUserId"; -import { User } from "../../types"; +import { User, UserContext } from "../../types"; import { logDebugMessage } from "../../logger"; export function validateAndNormaliseUserInput(config?: TypeInput): TypeNormalisedInput { @@ -69,7 +69,8 @@ export function isValidRecipeId(recipeId: string): recipeId is RecipeIdForUser { export async function getUserForRecipeId( recipeUserId: RecipeUserId, - recipeId: string + recipeId: string, + userContext: UserContext ): Promise<{ user: UserWithFirstAndLastName | undefined; recipe: @@ -80,7 +81,7 @@ export async function getUserForRecipeId( | "thirdpartypasswordless" | undefined; }> { - let userResponse = await _getUserForRecipeId(recipeUserId, recipeId); + let userResponse = await _getUserForRecipeId(recipeUserId, recipeId, userContext); let user: UserWithFirstAndLastName | undefined = undefined; if (userResponse.user !== undefined) { user = { @@ -97,7 +98,8 @@ export async function getUserForRecipeId( async function _getUserForRecipeId( recipeUserId: RecipeUserId, - recipeId: string + recipeId: string, + userContext: UserContext ): Promise<{ user: User | undefined; recipe: @@ -118,7 +120,7 @@ async function _getUserForRecipeId( const user = await AccountLinking.getInstance().recipeInterfaceImpl.getUser({ userId: recipeUserId.getAsString(), - userContext: {}, + userContext, }); if (user === undefined) { @@ -257,7 +259,11 @@ export function isRecipeInitialised(recipeId: RecipeIdForUser): boolean { return isRecipeInitialised; } -export async function validateApiKey(input: { req: BaseRequest; config: TypeNormalisedInput; userContext: any }) { +export async function validateApiKey(input: { + req: BaseRequest; + config: TypeNormalisedInput; + userContext: UserContext; +}) { let apiKeyHeaderValue: string | undefined = input.req.getHeaderValue("authorization"); // We receieve the api key as `Bearer API_KEY`, this retrieves just the key diff --git a/lib/ts/recipe/emailpassword/api/emailExists.ts b/lib/ts/recipe/emailpassword/api/emailExists.ts index 09253280d..3ed5ecab6 100644 --- a/lib/ts/recipe/emailpassword/api/emailExists.ts +++ b/lib/ts/recipe/emailpassword/api/emailExists.ts @@ -16,12 +16,13 @@ import { send200Response } from "../../../utils"; import STError from "../error"; import { APIInterface, APIOptions } from "../"; +import { UserContext } from "../../../types"; export default async function emailExists( apiImplementation: APIInterface, tenantId: string, options: APIOptions, - userContext: any + userContext: UserContext ): Promise { // Logic as per https://github.com/supertokens/supertokens-node/issues/47#issue-751571692 diff --git a/lib/ts/recipe/emailpassword/api/generatePasswordResetToken.ts b/lib/ts/recipe/emailpassword/api/generatePasswordResetToken.ts index 5db0fabc9..ddfbd08ba 100644 --- a/lib/ts/recipe/emailpassword/api/generatePasswordResetToken.ts +++ b/lib/ts/recipe/emailpassword/api/generatePasswordResetToken.ts @@ -16,12 +16,13 @@ import { send200Response } from "../../../utils"; import { validateFormFieldsOrThrowError } from "./utils"; import { APIInterface, APIOptions } from "../"; +import { UserContext } from "../../../types"; export default async function generatePasswordResetToken( apiImplementation: APIInterface, tenantId: string, options: APIOptions, - userContext: any + userContext: UserContext ): Promise { // Logic as per https://github.com/supertokens/supertokens-node/issues/22#issuecomment-710512442 diff --git a/lib/ts/recipe/emailpassword/api/implementation.ts b/lib/ts/recipe/emailpassword/api/implementation.ts index 0fa0e069c..f8967d4ef 100644 --- a/lib/ts/recipe/emailpassword/api/implementation.ts +++ b/lib/ts/recipe/emailpassword/api/implementation.ts @@ -2,13 +2,15 @@ import { APIInterface, APIOptions } from "../"; import { logDebugMessage } from "../../../logger"; import Session from "../../session"; import { SessionContainerInterface } from "../../session/types"; -import { GeneralErrorResponse, User } from "../../../types"; +import { GeneralErrorResponse, User, UserContext } from "../../../types"; import { listUsersByAccountInfo, getUser } from "../../../"; import AccountLinking from "../../accountlinking/recipe"; import EmailVerification from "../../emailverification/recipe"; import { RecipeLevelUser } from "../../accountlinking/types"; import RecipeUserId from "../../../recipeUserId"; import { getPasswordResetLink } from "../utils"; +import MultiFactorAuthRecipe from "../../multifactorauth/recipe"; +import { MFAFlowErrors } from "../../multifactorauth/types"; export default function getAPIImplementation(): APIInterface { return { @@ -19,7 +21,7 @@ export default function getAPIImplementation(): APIInterface { email: string; tenantId: string; options: APIOptions; - userContext: any; + userContext: UserContext; }): Promise< | { status: "OK"; @@ -62,7 +64,7 @@ export default function getAPIImplementation(): APIInterface { }[]; tenantId: string; options: APIOptions; - userContext: any; + userContext: UserContext; }): Promise< | { status: "OK"; @@ -346,7 +348,7 @@ export default function getAPIImplementation(): APIInterface { token: string; tenantId: string; options: APIOptions; - userContext: any; + userContext: UserContext; }): Promise< | { status: "OK"; @@ -567,7 +569,7 @@ export default function getAPIImplementation(): APIInterface { }[]; tenantId: string; options: APIOptions; - userContext: any; + userContext: UserContext; }): Promise< | { status: "OK"; @@ -581,6 +583,7 @@ export default function getAPIImplementation(): APIInterface { status: "SIGN_IN_NOT_ALLOWED"; reason: string; } + | MFAFlowErrors | GeneralErrorResponse > { let email = formFields.filter((f) => f.id === "email")[0].value; @@ -620,26 +623,82 @@ export default function getAPIImplementation(): APIInterface { }; } - // the above sign in recipe function does not do account linking - so we do it here. - response.user = await AccountLinking.getInstance().createPrimaryUserIdOrLinkAccounts({ + let session = await Session.getSession(options.req, options.res, { + sessionRequired: false, + overrideGlobalClaimValidators: () => [], + }); + + const mfaInstance = MultiFactorAuthRecipe.getInstance(); + + // we do not want to attempt accountlinking when there is an active session and MFA is turned on + if (session === undefined || mfaInstance === undefined) { + // the above sign in recipe function does not do account linking - so we do it here. + response.user = await AccountLinking.getInstance().createPrimaryUserIdOrLinkAccounts({ + tenantId, + user: response.user, + userContext, + }); + } + + if (mfaInstance === undefined) { + // No MFA stuff here, so we just create and return the session + let session = await Session.createNewOrKeepExistingSession( + options.req, + options.res, + tenantId, + emailPasswordRecipeUser.recipeUserId, + {}, + {}, + userContext + ); + + return { + status: "OK", + session, + user: response.user, + }; + } + + const mfaValidationRes = await mfaInstance.validateForMultifactorAuthBeforeFactorCompletion({ + req: options.req, + res: options.res, tenantId, - user: response.user, + factorIdInProgress: "emailpassword", + session, + userLoggingIn: response.user, + isAlreadySetup: true, userContext, }); - let session = await Session.createNewSession( - options.req, - options.res, + if (mfaValidationRes.status === "FACTOR_SETUP_NOT_ALLOWED_ERROR") { + throw new Error("Should never come here"); + } + + if (mfaValidationRes.status !== "OK") { + return mfaValidationRes; + } + + const sessionRes = await mfaInstance.createOrUpdateSessionForMultifactorAuthAfterFactorCompletion({ + req: options.req, + res: options.res, tenantId, - emailPasswordRecipeUser.recipeUserId, - {}, - {}, - userContext - ); + factorIdInProgress: "emailpassword", + justCompletedFactorUserInfo: { + user: response.user, + createdNewUser: false, + recipeUserId: emailPasswordRecipeUser.recipeUserId, + }, + userContext, + }); + + if (sessionRes.status !== "OK") { + return sessionRes; + } + return { status: "OK", - session, - user: response.user, + session: sessionRes.session, + user: (await getUser(response.user.id, userContext))!, // fetching user again cause the user might have been updated while setting up mfa }; }, @@ -655,7 +714,7 @@ export default function getAPIImplementation(): APIInterface { }[]; tenantId: string; options: APIOptions; - userContext: any; + userContext: UserContext; }): Promise< | { status: "OK"; @@ -669,6 +728,7 @@ export default function getAPIImplementation(): APIInterface { | { status: "EMAIL_ALREADY_EXISTS_ERROR"; } + | MFAFlowErrors | GeneralErrorResponse > { let email = formFields.filter((f) => f.id === "email")[0].value; @@ -718,11 +778,44 @@ export default function getAPIImplementation(): APIInterface { }; } + const mfaInstance = MultiFactorAuthRecipe.getInstance(); + if (mfaInstance !== undefined) { + let session = await Session.getSession(options.req, options.res, { + sessionRequired: false, + overrideGlobalClaimValidators: () => [], + }); + + const mfaValidationRes = await mfaInstance.validateForMultifactorAuthBeforeFactorCompletion({ + req: options.req, + res: options.res, + tenantId, + factorIdInProgress: "emailpassword", + session, + userLoggingIn: undefined, + isAlreadySetup: false, // since this is a sign up + signUpInfo: { + email, + isVerifiedFactor: false, + }, + userContext, + }); + if (mfaValidationRes.status !== "OK") { + return mfaValidationRes; + } + } + + let session = Session.getSession(options.req, options.res, { + sessionRequired: false, + overrideGlobalClaimValidators: () => [], + }); + // this function also does account linking let response = await options.recipeImplementation.signUp({ tenantId, email, password, + // we do not want to attempt accountlinking when there is an active session and MFA is turned on + shouldAttemptAccountLinkingIfAllowed: session === undefined || mfaInstance === undefined, userContext, }); if (response.status === "EMAIL_ALREADY_EXISTS_ERROR") { @@ -737,18 +830,46 @@ export default function getAPIImplementation(): APIInterface { throw new Error("Race condition error - please call this API again"); } - let session = await Session.createNewSession( - options.req, - options.res, + if (mfaInstance === undefined) { + // No MFA stuff here, so we just create and return the session + let session = await Session.createNewOrKeepExistingSession( + options.req, + options.res, + tenantId, + emailPasswordRecipeUser.recipeUserId, + {}, + {}, + userContext + ); + + return { + status: "OK", + session, + user: (await getUser(response.user.id, userContext))!, // fetching user again cause the user might have been updated while setting up mfa + }; + } + + const sessionRes = await mfaInstance.createOrUpdateSessionForMultifactorAuthAfterFactorCompletion({ + req: options.req, + res: options.res, tenantId, - emailPasswordRecipeUser.recipeUserId, - {}, - {}, - userContext - ); + factorIdInProgress: "emailpassword", + isAlreadySetup: false, + justCompletedFactorUserInfo: { + user: response.user, + createdNewUser: true, + recipeUserId: emailPasswordRecipeUser.recipeUserId, + }, + userContext, + }); + + if (sessionRes.status !== "OK") { + return sessionRes; + } + return { status: "OK", - session, + session: sessionRes.session, user: response.user, }; }, diff --git a/lib/ts/recipe/emailpassword/api/passwordReset.ts b/lib/ts/recipe/emailpassword/api/passwordReset.ts index f0ea38fa8..82d381012 100644 --- a/lib/ts/recipe/emailpassword/api/passwordReset.ts +++ b/lib/ts/recipe/emailpassword/api/passwordReset.ts @@ -17,12 +17,13 @@ import { send200Response } from "../../../utils"; import { validateFormFieldsOrThrowError } from "./utils"; import STError from "../error"; import { APIInterface, APIOptions } from "../"; +import { UserContext } from "../../../types"; export default async function passwordReset( apiImplementation: APIInterface, tenantId: string, options: APIOptions, - userContext: any + userContext: UserContext ): Promise { // Logic as per https://github.com/supertokens/supertokens-node/issues/22#issuecomment-710512442 diff --git a/lib/ts/recipe/emailpassword/api/signin.ts b/lib/ts/recipe/emailpassword/api/signin.ts index 58b3edfd5..17ec46443 100644 --- a/lib/ts/recipe/emailpassword/api/signin.ts +++ b/lib/ts/recipe/emailpassword/api/signin.ts @@ -16,12 +16,13 @@ import { getBackwardsCompatibleUserInfo, send200Response } from "../../../utils"; import { validateFormFieldsOrThrowError } from "./utils"; import { APIInterface, APIOptions } from "../"; +import { UserContext } from "../../../types"; export default async function signInAPI( apiImplementation: APIInterface, tenantId: string, options: APIOptions, - userContext: any + userContext: UserContext ): Promise { // Logic as per https://github.com/supertokens/supertokens-node/issues/20#issuecomment-710346362 if (apiImplementation.signInPOST === undefined) { diff --git a/lib/ts/recipe/emailpassword/api/signup.ts b/lib/ts/recipe/emailpassword/api/signup.ts index 4751237b1..2b48635d5 100644 --- a/lib/ts/recipe/emailpassword/api/signup.ts +++ b/lib/ts/recipe/emailpassword/api/signup.ts @@ -17,12 +17,13 @@ import { getBackwardsCompatibleUserInfo, send200Response } from "../../../utils" import { validateFormFieldsOrThrowError } from "./utils"; import { APIInterface, APIOptions } from "../"; import STError from "../error"; +import { UserContext } from "../../../types"; export default async function signUpAPI( apiImplementation: APIInterface, tenantId: string, options: APIOptions, - userContext: any + userContext: UserContext ): Promise { // Logic as per https://github.com/supertokens/supertokens-node/issues/21#issuecomment-710423536 diff --git a/lib/ts/recipe/emailpassword/emaildelivery/services/backwardCompatibility/index.ts b/lib/ts/recipe/emailpassword/emaildelivery/services/backwardCompatibility/index.ts index 3f975e117..81b300f23 100644 --- a/lib/ts/recipe/emailpassword/emaildelivery/services/backwardCompatibility/index.ts +++ b/lib/ts/recipe/emailpassword/emaildelivery/services/backwardCompatibility/index.ts @@ -14,7 +14,7 @@ */ import { TypeEmailPasswordEmailDeliveryInput } from "../../../types"; import { createAndSendEmailUsingSupertokensService } from "../../../passwordResetFunctions"; -import { NormalisedAppinfo } from "../../../../../types"; +import { NormalisedAppinfo, UserContext } from "../../../../../types"; import { EmailDeliveryInterface } from "../../../../../ingredients/emaildelivery/types"; export default class BackwardCompatibilityService @@ -27,7 +27,7 @@ export default class BackwardCompatibilityService this.appInfo = appInfo; } - sendEmail = async (input: TypeEmailPasswordEmailDeliveryInput & { userContext: any }) => { + sendEmail = async (input: TypeEmailPasswordEmailDeliveryInput & { userContext: UserContext }) => { // we add this here cause the user may have overridden the sendEmail function // to change the input email and if we don't do this, the input email // will get reset by the getUserById call above. diff --git a/lib/ts/recipe/emailpassword/emaildelivery/services/smtp/index.ts b/lib/ts/recipe/emailpassword/emaildelivery/services/smtp/index.ts index 974dfdb07..1f4f8693b 100644 --- a/lib/ts/recipe/emailpassword/emaildelivery/services/smtp/index.ts +++ b/lib/ts/recipe/emailpassword/emaildelivery/services/smtp/index.ts @@ -18,6 +18,7 @@ import { EmailDeliveryInterface } from "../../../../../ingredients/emaildelivery import { createTransport } from "nodemailer"; import OverrideableBuilder from "supertokens-js-override"; import { getServiceImplementation } from "./serviceImplementation"; +import { UserContext } from "../../../../../types"; export default class SMTPService implements EmailDeliveryInterface { serviceImpl: ServiceInterface; @@ -39,7 +40,7 @@ export default class SMTPService implements EmailDeliveryInterface { + sendEmail = async (input: TypeEmailPasswordEmailDeliveryInput & { userContext: UserContext }) => { let content = await this.serviceImpl.getContent(input); await this.serviceImpl.sendRawEmail({ ...content, diff --git a/lib/ts/recipe/emailpassword/emaildelivery/services/smtp/serviceImplementation/index.ts b/lib/ts/recipe/emailpassword/emaildelivery/services/smtp/serviceImplementation/index.ts index b16738eea..3a4c52dcd 100644 --- a/lib/ts/recipe/emailpassword/emaildelivery/services/smtp/serviceImplementation/index.ts +++ b/lib/ts/recipe/emailpassword/emaildelivery/services/smtp/serviceImplementation/index.ts @@ -21,6 +21,7 @@ import { GetContentResult, } from "../../../../../../ingredients/emaildelivery/services/smtp"; import getPasswordResetEmailContent from "../passwordReset"; +import { UserContext } from "../../../../../../types"; export function getServiceImplementation( transporter: Transporter, @@ -48,7 +49,7 @@ export function getServiceImplementation( } }, getContent: async function ( - input: TypeEmailPasswordEmailDeliveryInput & { userContext: any } + input: TypeEmailPasswordEmailDeliveryInput & { userContext: UserContext } ): Promise { return getPasswordResetEmailContent(input); }, diff --git a/lib/ts/recipe/emailpassword/index.ts b/lib/ts/recipe/emailpassword/index.ts index ac4de3649..75f5bbb99 100644 --- a/lib/ts/recipe/emailpassword/index.ts +++ b/lib/ts/recipe/emailpassword/index.ts @@ -20,27 +20,35 @@ import RecipeUserId from "../../recipeUserId"; import { DEFAULT_TENANT_ID } from "../multitenancy/constants"; import { getPasswordResetLink } from "./utils"; import { getRequestFromUserContext, getUser } from "../.."; +import { getUserContext } from "../../utils"; export default class Wrapper { static init = Recipe.init; static Error = SuperTokensError; - static signUp(tenantId: string, email: string, password: string, userContext?: any) { + static signUp( + tenantId: string, + email: string, + password: string, + shouldAttemptAccountLinkingIfAllowed?: boolean, + userContext?: Record + ) { return Recipe.getInstanceOrThrowError().recipeInterfaceImpl.signUp({ email, password, tenantId: tenantId === undefined ? DEFAULT_TENANT_ID : tenantId, - userContext: userContext === undefined ? {} : userContext, + shouldAttemptAccountLinkingIfAllowed, + userContext: getUserContext(userContext), }); } - static signIn(tenantId: string, email: string, password: string, userContext?: any) { + static signIn(tenantId: string, email: string, password: string, userContext?: Record) { return Recipe.getInstanceOrThrowError().recipeInterfaceImpl.signIn({ email, password, tenantId: tenantId === undefined ? DEFAULT_TENANT_ID : tenantId, - userContext: userContext === undefined ? {} : userContext, + userContext: getUserContext(userContext), }); } @@ -55,16 +63,26 @@ export default class Wrapper { * * And we want to allow primaryUserId being passed in. */ - static createResetPasswordToken(tenantId: string, userId: string, email: string, userContext?: any) { + static createResetPasswordToken( + tenantId: string, + userId: string, + email: string, + userContext?: Record + ) { return Recipe.getInstanceOrThrowError().recipeInterfaceImpl.createResetPasswordToken({ userId, email, tenantId: tenantId === undefined ? DEFAULT_TENANT_ID : tenantId, - userContext: userContext === undefined ? {} : userContext, + userContext: getUserContext(userContext), }); } - static async resetPasswordUsingToken(tenantId: string, token: string, newPassword: string, userContext?: any) { + static async resetPasswordUsingToken( + tenantId: string, + token: string, + newPassword: string, + userContext?: Record + ) { const consumeResp = await Wrapper.consumePasswordResetToken(tenantId, token, userContext); if (consumeResp.status !== "OK") { @@ -80,11 +98,11 @@ export default class Wrapper { }); } - static consumePasswordResetToken(tenantId: string, token: string, userContext?: any) { + static consumePasswordResetToken(tenantId: string, token: string, userContext?: Record) { return Recipe.getInstanceOrThrowError().recipeInterfaceImpl.consumePasswordResetToken({ token, tenantId: tenantId === undefined ? DEFAULT_TENANT_ID : tenantId, - userContext: userContext === undefined ? {} : userContext, + userContext: getUserContext(userContext), }); } @@ -92,13 +110,13 @@ export default class Wrapper { recipeUserId: RecipeUserId; email?: string; password?: string; - userContext?: any; + userContext?: Record; applyPasswordPolicy?: boolean; tenantIdForPasswordPolicy?: string; }) { return Recipe.getInstanceOrThrowError().recipeInterfaceImpl.updateEmailOrPassword({ ...input, - userContext: input.userContext ?? {}, + userContext: getUserContext(input.userContext), tenantIdForPasswordPolicy: input.tenantIdForPasswordPolicy === undefined ? DEFAULT_TENANT_ID : input.tenantIdForPasswordPolicy, }); @@ -108,9 +126,10 @@ export default class Wrapper { tenantId: string, userId: string, email: string, - userContext: any = {} + userContext?: Record ): Promise<{ status: "OK"; link: string } | { status: "UNKNOWN_USER_ID_ERROR" }> { - let token = await createResetPasswordToken(tenantId, userId, email, userContext); + const ctx = getUserContext(userContext); + let token = await createResetPasswordToken(tenantId, userId, email, ctx); if (token.status === "UNKNOWN_USER_ID_ERROR") { return token; } @@ -123,8 +142,8 @@ export default class Wrapper { recipeId: recipeInstance.getRecipeId(), token: token.token, tenantId: tenantId === undefined ? DEFAULT_TENANT_ID : tenantId, - request: getRequestFromUserContext(userContext), - userContext, + request: getRequestFromUserContext(ctx), + userContext: ctx, }), }; } @@ -133,7 +152,7 @@ export default class Wrapper { tenantId: string, userId: string, email: string, - userContext: any = {} + userContext?: Record ): Promise<{ status: "OK" | "UNKNOWN_USER_ID_ERROR" }> { const user = await getUser(userId, userContext); if (!user) { @@ -167,12 +186,12 @@ export default class Wrapper { }; } - static async sendEmail(input: TypeEmailPasswordEmailDeliveryInput & { userContext?: any }) { + static async sendEmail(input: TypeEmailPasswordEmailDeliveryInput & { userContext?: Record }) { let recipeInstance = Recipe.getInstanceOrThrowError(); return await recipeInstance.emailDelivery.ingredientInterfaceImpl.sendEmail({ - userContext: {}, ...input, tenantId: input.tenantId === undefined ? DEFAULT_TENANT_ID : input.tenantId, + userContext: getUserContext(input.userContext), }); } } diff --git a/lib/ts/recipe/emailpassword/recipe.ts b/lib/ts/recipe/emailpassword/recipe.ts index d5f194b51..2a3fa220a 100644 --- a/lib/ts/recipe/emailpassword/recipe.ts +++ b/lib/ts/recipe/emailpassword/recipe.ts @@ -15,7 +15,7 @@ import RecipeModule from "../../recipeModule"; import { TypeInput, TypeNormalisedInput, RecipeInterface, APIInterface } from "./types"; -import { NormalisedAppinfo, APIHandled, HTTPMethod, RecipeListFunction } from "../../types"; +import { NormalisedAppinfo, APIHandled, HTTPMethod, RecipeListFunction, UserContext } from "../../types"; import STError from "./error"; import { validateAndNormaliseUserInput } from "./utils"; import NormalisedURLPath from "../../normalisedURLPath"; @@ -39,6 +39,10 @@ import type { BaseRequest, BaseResponse } from "../../framework"; import OverrideableBuilder from "supertokens-js-override"; import EmailDeliveryIngredient from "../../ingredients/emaildelivery"; import { TypeEmailPasswordEmailDeliveryInput } from "./types"; +import { PostSuperTokensInitCallbacks } from "../../postSuperTokensInitCallbacks"; +import MultiFactorAuthRecipe from "../multifactorauth/recipe"; +import { User } from "../../user"; +import { TenantConfig } from "../multitenancy/types"; export default class Recipe extends RecipeModule { private static instance: Recipe | undefined = undefined; @@ -101,6 +105,39 @@ export default class Recipe extends RecipeModule { Recipe.instance = new Recipe(Recipe.RECIPE_ID, appInfo, isInServerlessEnv, config, { emailDelivery: undefined, }); + + PostSuperTokensInitCallbacks.addPostInitCallback(() => { + const mfaInstance = MultiFactorAuthRecipe.getInstance(); + if (mfaInstance !== undefined) { + mfaInstance.addGetAllFactorsFromOtherRecipesFunc((tenantConfig) => { + if (tenantConfig.passwordless.enabled === false) { + return { + factorIds: [], + firstFactorIds: [], + }; + } + return { + factorIds: ["emailpassword"], + firstFactorIds: ["emailpassword"], + }; + }); + mfaInstance.addGetFactorsSetupForUserFromOtherRecipes( + async (user: User, tenantConfig: TenantConfig) => { + if (tenantConfig.emailPassword.enabled === false) { + return []; + } + + for (const loginMethod of user.loginMethods) { + if (loginMethod.recipeId === Recipe.RECIPE_ID) { + return ["emailpassword"]; + } + } + return []; + } + ); + } + }); + return Recipe.instance; } else { throw new Error("Emailpassword recipe has already been initialised. Please check your code for bugs."); @@ -159,7 +196,7 @@ export default class Recipe extends RecipeModule { res: BaseResponse, _path: NormalisedURLPath, _method: HTTPMethod, - userContext: any + userContext: UserContext ): Promise => { let options = { config: this.config, diff --git a/lib/ts/recipe/emailpassword/recipeImplementation.ts b/lib/ts/recipe/emailpassword/recipeImplementation.ts index d8225cb6a..28e02ff60 100644 --- a/lib/ts/recipe/emailpassword/recipeImplementation.ts +++ b/lib/ts/recipe/emailpassword/recipeImplementation.ts @@ -6,7 +6,7 @@ import { getUser } from "../.."; import { FORM_FIELD_PASSWORD_ID } from "./constants"; import RecipeUserId from "../../recipeUserId"; import { DEFAULT_TENANT_ID } from "../multitenancy/constants"; -import { User as UserType } from "../../types"; +import { UserContext, User as UserType } from "../../types"; import { LoginMethod, User } from "../../user"; export default function getRecipeInterface( @@ -20,19 +20,20 @@ export default function getRecipeInterface( email, password, tenantId, + shouldAttemptAccountLinkingIfAllowed, userContext, }: { email: string; password: string; tenantId: string; - userContext: any; + shouldAttemptAccountLinkingIfAllowed?: boolean; + userContext: UserContext; } ): Promise< | { status: "OK"; user: UserType; recipeUserId: RecipeUserId; - isValidFirstFactorForTenant: boolean | undefined; } | { status: "EMAIL_ALREADY_EXISTS_ERROR" } > { @@ -46,17 +47,20 @@ export default function getRecipeInterface( return response; } - let updatedUser = await AccountLinking.getInstance().createPrimaryUserIdOrLinkAccounts({ - tenantId, - user: response.user, - userContext, - }); + let updatedUser = response.user; + + if (shouldAttemptAccountLinkingIfAllowed ?? true) { + updatedUser = await AccountLinking.getInstance().createPrimaryUserIdOrLinkAccounts({ + tenantId, + user: response.user, + userContext, + }); + } return { status: "OK", user: updatedUser, recipeUserId: response.recipeUserId, - isValidFirstFactorForTenant: response.isValidFirstFactorForTenant, }; }, @@ -64,13 +68,12 @@ export default function getRecipeInterface( tenantId: string; email: string; password: string; - userContext: any; + userContext: UserContext; }): Promise< | { status: "OK"; user: User; recipeUserId: RecipeUserId; - isValidFirstFactorForTenant: boolean | undefined; } | { status: "EMAIL_ALREADY_EXISTS_ERROR" } > { @@ -89,7 +92,6 @@ export default function getRecipeInterface( status: "OK", user: new User(resp.user), recipeUserId: new RecipeUserId(resp.recipeUserId), - isValidFirstFactorForTenant: resp.isValidFirstFactorForTenant, }; } return resp; @@ -107,13 +109,12 @@ export default function getRecipeInterface( email: string; password: string; tenantId: string; - userContext: any; + userContext: UserContext; }): Promise< | { status: "OK"; user: UserType; recipeUserId: RecipeUserId; - isValidFirstFactorForTenant: boolean | undefined; } | { status: "WRONG_CREDENTIALS_ERROR" } > { @@ -169,7 +170,7 @@ export default function getRecipeInterface( userId: string; email: string; tenantId: string; - userContext: any; + userContext: UserContext; }): Promise<{ status: "OK"; token: string } | { status: "UNKNOWN_USER_ID_ERROR" }> { // the input user ID can be a recipe or a primary user ID. return await querier.sendPostRequest( @@ -191,7 +192,7 @@ export default function getRecipeInterface( }: { token: string; tenantId: string; - userContext: any; + userContext: UserContext; }): Promise< | { status: "OK"; @@ -218,7 +219,7 @@ export default function getRecipeInterface( password?: string; applyPasswordPolicy?: boolean; tenantIdForPasswordPolicy: string; - userContext: any; + userContext: UserContext; }): Promise< | { status: "OK" | "UNKNOWN_USER_ID_ERROR" | "EMAIL_ALREADY_EXISTS_ERROR"; diff --git a/lib/ts/recipe/emailpassword/types.ts b/lib/ts/recipe/emailpassword/types.ts index fd6851376..98fa654c1 100644 --- a/lib/ts/recipe/emailpassword/types.ts +++ b/lib/ts/recipe/emailpassword/types.ts @@ -21,8 +21,9 @@ import { TypeInputWithService as EmailDeliveryTypeInputWithService, } from "../../ingredients/emaildelivery/types"; import EmailDeliveryIngredient from "../../ingredients/emaildelivery"; -import { GeneralErrorResponse, NormalisedAppinfo, User } from "../../types"; +import { GeneralErrorResponse, NormalisedAppinfo, User, UserContext } from "../../types"; import RecipeUserId from "../../recipeUserId"; +import { MFAFlowErrors } from "../multifactorauth/types"; export type TypeNormalisedInput = { signUpFeature: TypeNormalisedInputSignUp; @@ -88,13 +89,13 @@ export type RecipeInterface = { email: string; password: string; tenantId: string; - userContext: any; + shouldAttemptAccountLinkingIfAllowed?: boolean; + userContext: UserContext; }): Promise< | { status: "OK"; user: User; recipeUserId: RecipeUserId; - isValidFirstFactorForTenant: boolean | undefined; } | { status: "EMAIL_ALREADY_EXISTS_ERROR" } >; @@ -107,13 +108,12 @@ export type RecipeInterface = { email: string; password: string; tenantId: string; - userContext: any; + userContext: UserContext; }): Promise< | { status: "OK"; user: User; recipeUserId: RecipeUserId; - isValidFirstFactorForTenant: boolean | undefined; } | { status: "EMAIL_ALREADY_EXISTS_ERROR" } >; @@ -122,11 +122,8 @@ export type RecipeInterface = { email: string; password: string; tenantId: string; - userContext: any; - }): Promise< - | { status: "OK"; user: User; recipeUserId: RecipeUserId; isValidFirstFactorForTenant: boolean | undefined } - | { status: "WRONG_CREDENTIALS_ERROR" } - >; + userContext: UserContext; + }): Promise<{ status: "OK"; user: User; recipeUserId: RecipeUserId } | { status: "WRONG_CREDENTIALS_ERROR" }>; /** * We pass in the email as well to this function cause the input userId @@ -137,13 +134,13 @@ export type RecipeInterface = { userId: string; // the id can be either recipeUserId or primaryUserId email: string; tenantId: string; - userContext: any; + userContext: UserContext; }): Promise<{ status: "OK"; token: string } | { status: "UNKNOWN_USER_ID_ERROR" }>; consumePasswordResetToken(input: { token: string; tenantId: string; - userContext: any; + userContext: UserContext; }): Promise< | { status: "OK"; @@ -159,7 +156,7 @@ export type RecipeInterface = { // for which one to update the password for. email?: string; password?: string; - userContext: any; + userContext: UserContext; applyPasswordPolicy?: boolean; tenantIdForPasswordPolicy: string; }): Promise< @@ -192,7 +189,7 @@ export type APIInterface = { email: string; tenantId: string; options: APIOptions; - userContext: any; + userContext: UserContext; }) => Promise< | { status: "OK"; @@ -210,7 +207,7 @@ export type APIInterface = { }[]; tenantId: string; options: APIOptions; - userContext: any; + userContext: UserContext; }) => Promise< | { status: "OK"; @@ -232,7 +229,7 @@ export type APIInterface = { token: string; tenantId: string; options: APIOptions; - userContext: any; + userContext: UserContext; }) => Promise< | { status: "OK"; @@ -255,7 +252,7 @@ export type APIInterface = { }[]; tenantId: string; options: APIOptions; - userContext: any; + userContext: UserContext; }) => Promise< | { status: "OK"; @@ -269,6 +266,7 @@ export type APIInterface = { | { status: "WRONG_CREDENTIALS_ERROR"; } + | MFAFlowErrors | GeneralErrorResponse >); @@ -281,7 +279,7 @@ export type APIInterface = { }[]; tenantId: string; options: APIOptions; - userContext: any; + userContext: UserContext; }) => Promise< | { status: "OK"; @@ -295,6 +293,7 @@ export type APIInterface = { | { status: "EMAIL_ALREADY_EXISTS_ERROR"; } + | MFAFlowErrors | GeneralErrorResponse >); }; diff --git a/lib/ts/recipe/emailpassword/utils.ts b/lib/ts/recipe/emailpassword/utils.ts index 9a31c7348..05e507c45 100644 --- a/lib/ts/recipe/emailpassword/utils.ts +++ b/lib/ts/recipe/emailpassword/utils.ts @@ -24,7 +24,7 @@ import { NormalisedFormField, TypeInputFormField, } from "./types"; -import { NormalisedAppinfo } from "../../types"; +import { NormalisedAppinfo, UserContext } from "../../types"; import { FORM_FIELD_EMAIL_ID, FORM_FIELD_PASSWORD_ID } from "./constants"; import { RecipeInterface, APIInterface } from "./types"; import BackwardCompatibilityService from "./emaildelivery/services/backwardCompatibility"; @@ -255,7 +255,7 @@ export function getPasswordResetLink(input: { recipeId: string; tenantId: string; request: BaseRequest | undefined; - userContext: any; + userContext: UserContext; }): string { return ( input.appInfo diff --git a/lib/ts/recipe/emailverification/api/emailVerify.ts b/lib/ts/recipe/emailverification/api/emailVerify.ts index e8a592fed..2924dd5b5 100644 --- a/lib/ts/recipe/emailverification/api/emailVerify.ts +++ b/lib/ts/recipe/emailverification/api/emailVerify.ts @@ -17,12 +17,13 @@ import { send200Response, normaliseHttpMethod } from "../../../utils"; import STError from "../error"; import { APIInterface, APIOptions } from "../"; import Session from "../../session"; +import { UserContext } from "../../../types"; export default async function emailVerify( apiImplementation: APIInterface, tenantId: string, options: APIOptions, - userContext: any + userContext: UserContext ): Promise { let result; diff --git a/lib/ts/recipe/emailverification/api/generateEmailVerifyToken.ts b/lib/ts/recipe/emailverification/api/generateEmailVerifyToken.ts index ed53e92bd..2c1021059 100644 --- a/lib/ts/recipe/emailverification/api/generateEmailVerifyToken.ts +++ b/lib/ts/recipe/emailverification/api/generateEmailVerifyToken.ts @@ -16,11 +16,12 @@ import { send200Response } from "../../../utils"; import { APIInterface, APIOptions } from "../"; import Session from "../../session"; +import { UserContext } from "../../../types"; export default async function generateEmailVerifyToken( apiImplementation: APIInterface, options: APIOptions, - userContext: any + userContext: UserContext ): Promise { // Logic as per https://github.com/supertokens/supertokens-node/issues/62#issuecomment-751616106 diff --git a/lib/ts/recipe/emailverification/emailVerificationClaim.ts b/lib/ts/recipe/emailverification/emailVerificationClaim.ts index abec3146d..d4e6e44e6 100644 --- a/lib/ts/recipe/emailverification/emailVerificationClaim.ts +++ b/lib/ts/recipe/emailverification/emailVerificationClaim.ts @@ -9,7 +9,7 @@ export class EmailVerificationClaimClass extends BooleanClaim { constructor() { super({ key: "st-ev", - async fetchValue(_userId, recipeUserId, __tenantId, userContext) { + async fetchValue(_userId, recipeUserId, __tenantId, _currentPayload, userContext) { const recipe = EmailVerificationRecipe.getInstanceOrThrowError(); let emailInfo = await recipe.getEmailForRecipeUserId(undefined, recipeUserId, userContext); diff --git a/lib/ts/recipe/emailverification/emaildelivery/services/backwardCompatibility/index.ts b/lib/ts/recipe/emailverification/emaildelivery/services/backwardCompatibility/index.ts index 7e97fd89f..ae991a7c5 100644 --- a/lib/ts/recipe/emailverification/emaildelivery/services/backwardCompatibility/index.ts +++ b/lib/ts/recipe/emailverification/emaildelivery/services/backwardCompatibility/index.ts @@ -14,7 +14,7 @@ */ import { TypeEmailVerificationEmailDeliveryInput } from "../../../types"; import { createAndSendEmailUsingSupertokensService } from "../../../emailVerificationFunctions"; -import { NormalisedAppinfo } from "../../../../../types"; +import { NormalisedAppinfo, UserContext } from "../../../../../types"; import { EmailDeliveryInterface } from "../../../../../ingredients/emaildelivery/types"; export default class BackwardCompatibilityService @@ -27,7 +27,7 @@ export default class BackwardCompatibilityService this.isInServerlessEnv = isInServerlessEnv; } - sendEmail = async (input: TypeEmailVerificationEmailDeliveryInput & { userContext: any }) => { + sendEmail = async (input: TypeEmailVerificationEmailDeliveryInput & { userContext: UserContext }) => { try { if (!this.isInServerlessEnv) { createAndSendEmailUsingSupertokensService( diff --git a/lib/ts/recipe/emailverification/emaildelivery/services/smtp/index.ts b/lib/ts/recipe/emailverification/emaildelivery/services/smtp/index.ts index 6adb2549e..a84df0fbb 100644 --- a/lib/ts/recipe/emailverification/emaildelivery/services/smtp/index.ts +++ b/lib/ts/recipe/emailverification/emaildelivery/services/smtp/index.ts @@ -18,6 +18,7 @@ import { TypeEmailVerificationEmailDeliveryInput } from "../../../types"; import { createTransport } from "nodemailer"; import OverrideableBuilder from "supertokens-js-override"; import { getServiceImplementation } from "./serviceImplementation"; +import { UserContext } from "../../../../../types"; export default class SMTPService implements EmailDeliveryInterface { serviceImpl: ServiceInterface; @@ -39,7 +40,7 @@ export default class SMTPService implements EmailDeliveryInterface { + sendEmail = async (input: TypeEmailVerificationEmailDeliveryInput & { userContext: UserContext }) => { let content = await this.serviceImpl.getContent(input); await this.serviceImpl.sendRawEmail({ ...content, diff --git a/lib/ts/recipe/emailverification/emaildelivery/services/smtp/serviceImplementation.ts b/lib/ts/recipe/emailverification/emaildelivery/services/smtp/serviceImplementation.ts index 470415f0c..2714e18ef 100644 --- a/lib/ts/recipe/emailverification/emaildelivery/services/smtp/serviceImplementation.ts +++ b/lib/ts/recipe/emailverification/emaildelivery/services/smtp/serviceImplementation.ts @@ -21,6 +21,7 @@ import { GetContentResult, } from "../../../../../ingredients/emaildelivery/services/smtp"; import getEmailVerifyEmailContent from "./emailVerify"; +import { UserContext } from "../../../../../types"; export function getServiceImplementation( transporter: Transporter, @@ -48,7 +49,7 @@ export function getServiceImplementation( } }, getContent: async function ( - input: TypeEmailVerificationEmailDeliveryInput & { userContext: any } + input: TypeEmailVerificationEmailDeliveryInput & { userContext: UserContext } ): Promise { return getEmailVerifyEmailContent(input); }, diff --git a/lib/ts/recipe/emailverification/index.ts b/lib/ts/recipe/emailverification/index.ts index ab6b8645b..c990709ed 100644 --- a/lib/ts/recipe/emailverification/index.ts +++ b/lib/ts/recipe/emailverification/index.ts @@ -26,6 +26,7 @@ import { EmailVerificationClaim } from "./emailVerificationClaim"; import RecipeUserId from "../../recipeUserId"; import { getEmailVerifyLink } from "./utils"; import { getRequestFromUserContext } from "../.."; +import { getUserContext } from "../../utils"; export default class Wrapper { static init = Recipe.init; @@ -38,7 +39,7 @@ export default class Wrapper { tenantId: string, recipeUserId: RecipeUserId, email?: string, - userContext: any = {} + userContext?: Record ): Promise< | { status: "OK"; @@ -46,10 +47,11 @@ export default class Wrapper { } | { status: "EMAIL_ALREADY_VERIFIED_ERROR" } > { + const ctx = getUserContext(userContext); const recipeInstance = Recipe.getInstanceOrThrowError(); if (email === undefined) { - const emailInfo = await recipeInstance.getEmailForRecipeUserId(undefined, recipeUserId, userContext); + const emailInfo = await recipeInstance.getEmailForRecipeUserId(undefined, recipeUserId, ctx); if (emailInfo.status === "OK") { email = emailInfo.email; } else if (emailInfo.status === "EMAIL_DOES_NOT_EXIST_ERROR") { @@ -65,7 +67,7 @@ export default class Wrapper { recipeUserId, email: email!, tenantId, - userContext, + userContext: ctx, }); } @@ -73,7 +75,7 @@ export default class Wrapper { tenantId: string, recipeUserId: RecipeUserId, email?: string, - userContext: any = {} + userContext?: Record ): Promise< | { status: "OK"; @@ -81,10 +83,11 @@ export default class Wrapper { } | { status: "EMAIL_ALREADY_VERIFIED_ERROR" } > { + const ctx = getUserContext(userContext); const recipeInstance = Recipe.getInstanceOrThrowError(); const appInfo = recipeInstance.getAppInfo(); - let emailVerificationToken = await createEmailVerificationToken(tenantId, recipeUserId, email, userContext); + let emailVerificationToken = await createEmailVerificationToken(tenantId, recipeUserId, email, ctx); if (emailVerificationToken.status === "EMAIL_ALREADY_VERIFIED_ERROR") { return { status: "EMAIL_ALREADY_VERIFIED_ERROR", @@ -98,8 +101,8 @@ export default class Wrapper { token: emailVerificationToken.token, recipeId: recipeInstance.getRecipeId(), tenantId, - request: getRequestFromUserContext(userContext), - userContext, + request: getRequestFromUserContext(ctx), + userContext: ctx, }), }; } @@ -109,17 +112,18 @@ export default class Wrapper { userId: string, recipeUserId: RecipeUserId, email?: string, - userContext: any = {} + userContext?: Record ): Promise< | { status: "OK"; } | { status: "EMAIL_ALREADY_VERIFIED_ERROR" } > { + const ctx = getUserContext(userContext); if (email === undefined) { const recipeInstance = Recipe.getInstanceOrThrowError(); - const emailInfo = await recipeInstance.getEmailForRecipeUserId(undefined, recipeUserId, userContext); + const emailInfo = await recipeInstance.getEmailForRecipeUserId(undefined, recipeUserId, ctx); if (emailInfo.status === "OK") { email = emailInfo.email; } else if (emailInfo.status === "EMAIL_DOES_NOT_EXIST_ERROR") { @@ -131,7 +135,7 @@ export default class Wrapper { } } - let emailVerificationLink = await this.createEmailVerificationLink(tenantId, recipeUserId, email, userContext); + let emailVerificationLink = await this.createEmailVerificationLink(tenantId, recipeUserId, email, ctx); if (emailVerificationLink.status === "EMAIL_ALREADY_VERIFIED_ERROR") { return { @@ -148,6 +152,7 @@ export default class Wrapper { }, emailVerifyLink: emailVerificationLink.link, tenantId, + userContext: ctx, }); return { @@ -159,20 +164,21 @@ export default class Wrapper { tenantId: string, token: string, attemptAccountLinking: boolean = true, - userContext: any = {} + userContext?: Record ) { return await Recipe.getInstanceOrThrowError().recipeInterfaceImpl.verifyEmailUsingToken({ token, tenantId, attemptAccountLinking, - userContext, + userContext: getUserContext(userContext), }); } - static async isEmailVerified(recipeUserId: RecipeUserId, email?: string, userContext: any = {}) { + static async isEmailVerified(recipeUserId: RecipeUserId, email?: string, userContext?: Record) { + const ctx = getUserContext(userContext); const recipeInstance = Recipe.getInstanceOrThrowError(); if (email === undefined) { - const emailInfo = await recipeInstance.getEmailForRecipeUserId(undefined, recipeUserId, userContext); + const emailInfo = await recipeInstance.getEmailForRecipeUserId(undefined, recipeUserId, ctx); if (emailInfo.status === "OK") { email = emailInfo.email; @@ -186,7 +192,7 @@ export default class Wrapper { return await recipeInstance.recipeInterfaceImpl.isEmailVerified({ recipeUserId, email, - userContext, + userContext: ctx, }); } @@ -194,15 +200,16 @@ export default class Wrapper { tenantId: string, recipeUserId: RecipeUserId, email?: string, - userContext: any = {} + userContext?: Record ) { + const ctx = getUserContext(userContext); const recipeInstance = Recipe.getInstanceOrThrowError(); // If the dev wants to delete the tokens for an old email address of the user they can pass the address // but redeeming those tokens would have no effect on isEmailVerified called without the old address // so in general that is not necessary either. if (email === undefined) { - const emailInfo = await recipeInstance.getEmailForRecipeUserId(undefined, recipeUserId, userContext); + const emailInfo = await recipeInstance.getEmailForRecipeUserId(undefined, recipeUserId, ctx); if (emailInfo.status === "OK") { email = emailInfo.email; } else if (emailInfo.status === "EMAIL_DOES_NOT_EXIST_ERROR") { @@ -220,14 +227,15 @@ export default class Wrapper { recipeUserId, email: email!, tenantId, - userContext, + userContext: ctx, }); } - static async unverifyEmail(recipeUserId: RecipeUserId, email?: string, userContext: any = {}) { + static async unverifyEmail(recipeUserId: RecipeUserId, email?: string, userContext?: Record) { + const ctx = getUserContext(userContext); const recipeInstance = Recipe.getInstanceOrThrowError(); if (email === undefined) { - const emailInfo = await recipeInstance.getEmailForRecipeUserId(undefined, recipeUserId, userContext); + const emailInfo = await recipeInstance.getEmailForRecipeUserId(undefined, recipeUserId, ctx); if (emailInfo.status === "OK") { email = emailInfo.email; } else if (emailInfo.status === "EMAIL_DOES_NOT_EXIST_ERROR") { @@ -242,15 +250,15 @@ export default class Wrapper { return await recipeInstance.recipeInterfaceImpl.unverifyEmail({ recipeUserId, email, - userContext, + userContext: ctx, }); } - static async sendEmail(input: TypeEmailVerificationEmailDeliveryInput & { userContext?: any }) { + static async sendEmail(input: TypeEmailVerificationEmailDeliveryInput & { userContext?: Record }) { let recipeInstance = Recipe.getInstanceOrThrowError(); return await recipeInstance.emailDelivery.ingredientInterfaceImpl.sendEmail({ ...input, - userContext: input.userContext ?? {}, + userContext: getUserContext(input.userContext), }); } } diff --git a/lib/ts/recipe/emailverification/recipe.ts b/lib/ts/recipe/emailverification/recipe.ts index 18abe8182..44476ee7c 100644 --- a/lib/ts/recipe/emailverification/recipe.ts +++ b/lib/ts/recipe/emailverification/recipe.ts @@ -15,7 +15,7 @@ import RecipeModule from "../../recipeModule"; import { TypeInput, TypeNormalisedInput, RecipeInterface, APIInterface, GetEmailForRecipeUserIdFunc } from "./types"; -import { NormalisedAppinfo, APIHandled, RecipeListFunction, HTTPMethod } from "../../types"; +import { NormalisedAppinfo, APIHandled, RecipeListFunction, HTTPMethod, UserContext } from "../../types"; import STError from "./error"; import { validateAndNormaliseUserInput } from "./utils"; import NormalisedURLPath from "../../normalisedURLPath"; @@ -163,7 +163,7 @@ export default class Recipe extends RecipeModule { res: BaseResponse, _: NormalisedURLPath, __: HTTPMethod, - userContext: any + userContext: UserContext ): Promise => { let options = { config: this.config, @@ -233,7 +233,7 @@ export default class Recipe extends RecipeModule { }; }; - getPrimaryUserIdForRecipeUser = async (recipeUserId: RecipeUserId, userContext: any): Promise => { + getPrimaryUserIdForRecipeUser = async (recipeUserId: RecipeUserId, userContext: UserContext): Promise => { // We extract this into its own function like this cause we want to make sure that // this recipe does not get the email of the user ID from the getUser function. // In fact, there is a test "email verification recipe uses getUser function only in getEmailForRecipeUserId" @@ -260,7 +260,7 @@ export default class Recipe extends RecipeModule { res: BaseResponse; session: SessionContainerInterface | undefined; recipeUserIdWhoseEmailGotVerified: RecipeUserId; - userContext: any; + userContext: UserContext; }): Promise => { let primaryUserId = await this.getPrimaryUserIdForRecipeUser( input.recipeUserIdWhoseEmailGotVerified, diff --git a/lib/ts/recipe/emailverification/recipeImplementation.ts b/lib/ts/recipe/emailverification/recipeImplementation.ts index db97f8788..af5de304a 100644 --- a/lib/ts/recipe/emailverification/recipeImplementation.ts +++ b/lib/ts/recipe/emailverification/recipeImplementation.ts @@ -5,6 +5,7 @@ import RecipeUserId from "../../recipeUserId"; import { GetEmailForRecipeUserIdFunc, UserEmailInfo } from "./types"; import type AccountLinkingRecipe from "../accountlinking/recipe"; import { getUser } from "../.."; +import { UserContext } from "../../types"; export default function getRecipeInterface( querier: Querier, @@ -20,7 +21,7 @@ export default function getRecipeInterface( recipeUserId: RecipeUserId; email: string; tenantId: string; - userContext: any; + userContext: UserContext; }): Promise< | { status: "OK"; @@ -57,7 +58,7 @@ export default function getRecipeInterface( token: string; attemptAccountLinking: boolean; tenantId: string; - userContext: any; + userContext: UserContext; }): Promise<{ status: "OK"; user: UserEmailInfo } | { status: "EMAIL_VERIFICATION_INVALID_TOKEN_ERROR" }> { let response = await querier.sendPostRequest( new NormalisedURLPath(`/${tenantId}/recipe/user/email/verify`), @@ -113,7 +114,7 @@ export default function getRecipeInterface( }: { recipeUserId: RecipeUserId; email: string; - userContext: any; + userContext: UserContext; }): Promise { let response = await querier.sendGetRequest( new NormalisedURLPath("/recipe/user/email/verify"), @@ -130,7 +131,7 @@ export default function getRecipeInterface( recipeUserId: RecipeUserId; email: string; tenantId: string; - userContext: any; + userContext: UserContext; }): Promise<{ status: "OK" }> { await querier.sendPostRequest( new NormalisedURLPath(`/${input.tenantId}/recipe/user/email/verify/token/remove`), @@ -146,7 +147,7 @@ export default function getRecipeInterface( unverifyEmail: async function (input: { recipeUserId: RecipeUserId; email: string; - userContext: any; + userContext: UserContext; }): Promise<{ status: "OK" }> { await querier.sendPostRequest( new NormalisedURLPath("/recipe/user/email/verify/remove"), diff --git a/lib/ts/recipe/emailverification/types.ts b/lib/ts/recipe/emailverification/types.ts index 06d3bd5fa..639de13a4 100644 --- a/lib/ts/recipe/emailverification/types.ts +++ b/lib/ts/recipe/emailverification/types.ts @@ -20,7 +20,7 @@ import { TypeInputWithService as EmailDeliveryTypeInputWithService, } from "../../ingredients/emaildelivery/types"; import EmailDeliveryIngredient from "../../ingredients/emaildelivery"; -import { GeneralErrorResponse, NormalisedAppinfo } from "../../types"; +import { GeneralErrorResponse, NormalisedAppinfo, UserContext } from "../../types"; import { SessionContainerInterface } from "../session/types"; import RecipeUserId from "../../recipeUserId"; import { User } from "../../types"; @@ -30,7 +30,7 @@ export type TypeInput = { emailDelivery?: EmailDeliveryTypeInput; getEmailForRecipeUserId?: ( recipeUserId: RecipeUserId, - userContext: any + userContext: UserContext ) => Promise< | { status: "OK"; @@ -54,7 +54,7 @@ export type TypeNormalisedInput = { ) => EmailDeliveryTypeInputWithService; getEmailForRecipeUserId?: ( recipeUserId: RecipeUserId, - userContext: any + userContext: UserContext ) => Promise< | { status: "OK"; @@ -81,7 +81,7 @@ export type RecipeInterface = { recipeUserId: RecipeUserId; // must be a recipeUserId email: string; tenantId: string; - userContext: any; + userContext: UserContext; }): Promise< | { status: "OK"; @@ -94,19 +94,23 @@ export type RecipeInterface = { token: string; attemptAccountLinking: boolean; tenantId: string; - userContext: any; + userContext: UserContext; }): Promise<{ status: "OK"; user: UserEmailInfo } | { status: "EMAIL_VERIFICATION_INVALID_TOKEN_ERROR" }>; - isEmailVerified(input: { recipeUserId: RecipeUserId; email: string; userContext: any }): Promise; + isEmailVerified(input: { recipeUserId: RecipeUserId; email: string; userContext: UserContext }): Promise; revokeEmailVerificationTokens(input: { recipeUserId: RecipeUserId; email: string; tenantId: string; - userContext: any; + userContext: UserContext; }): Promise<{ status: "OK" }>; - unverifyEmail(input: { recipeUserId: RecipeUserId; email: string; userContext: any }): Promise<{ status: "OK" }>; + unverifyEmail(input: { + recipeUserId: RecipeUserId; + email: string; + userContext: UserContext; + }): Promise<{ status: "OK" }>; }; export type APIOptions = { @@ -127,7 +131,7 @@ export type APIInterface = { token: string; tenantId: string; options: APIOptions; - userContext: any; + userContext: UserContext; session?: SessionContainerInterface; }) => Promise< | { status: "OK"; user: UserEmailInfo; newSession?: SessionContainerInterface } @@ -139,7 +143,7 @@ export type APIInterface = { | undefined | ((input: { options: APIOptions; - userContext: any; + userContext: UserContext; session: SessionContainerInterface; }) => Promise< | { @@ -154,7 +158,7 @@ export type APIInterface = { | undefined | ((input: { options: APIOptions; - userContext: any; + userContext: UserContext; session: SessionContainerInterface; }) => Promise< | { status: "OK" } @@ -180,7 +184,7 @@ export type TypeEmailVerificationEmailDeliveryInput = { export type GetEmailForRecipeUserIdFunc = ( user: User | undefined, recipeUserId: RecipeUserId, - userContext: any + userContext: UserContext ) => Promise< | { status: "OK"; diff --git a/lib/ts/recipe/emailverification/utils.ts b/lib/ts/recipe/emailverification/utils.ts index 358876c4c..68efd512e 100644 --- a/lib/ts/recipe/emailverification/utils.ts +++ b/lib/ts/recipe/emailverification/utils.ts @@ -15,7 +15,7 @@ import Recipe from "./recipe"; import { TypeInput, TypeNormalisedInput, RecipeInterface, APIInterface } from "./types"; -import { NormalisedAppinfo } from "../../types"; +import { NormalisedAppinfo, UserContext } from "../../types"; import BackwardCompatibilityService from "./emaildelivery/services/backwardCompatibility"; import { BaseRequest } from "../../framework"; @@ -70,7 +70,7 @@ export function getEmailVerifyLink(input: { recipeId: string; tenantId: string; request: BaseRequest | undefined; - userContext: any; + userContext: UserContext; }): string { return ( input.appInfo diff --git a/lib/ts/recipe/jwt/api/getJWKS.ts b/lib/ts/recipe/jwt/api/getJWKS.ts index 0a88d6184..addb911f8 100644 --- a/lib/ts/recipe/jwt/api/getJWKS.ts +++ b/lib/ts/recipe/jwt/api/getJWKS.ts @@ -13,13 +13,14 @@ * under the License. */ +import { UserContext } from "../../../types"; import { send200Response } from "../../../utils"; import { APIInterface, APIOptions } from "../types"; export default async function getJWKS( apiImplementation: APIInterface, options: APIOptions, - userContext: any + userContext: UserContext ): Promise { if (apiImplementation.getJWKSGET === undefined) { return false; diff --git a/lib/ts/recipe/jwt/api/implementation.ts b/lib/ts/recipe/jwt/api/implementation.ts index 012d2aa22..4308c7cf1 100644 --- a/lib/ts/recipe/jwt/api/implementation.ts +++ b/lib/ts/recipe/jwt/api/implementation.ts @@ -14,7 +14,7 @@ */ import { APIInterface, APIOptions, JsonWebKey } from "../types"; -import { GeneralErrorResponse } from "../../../types"; +import { GeneralErrorResponse, UserContext } from "../../../types"; export default function getAPIImplementation(): APIInterface { return { @@ -23,7 +23,7 @@ export default function getAPIImplementation(): APIInterface { userContext, }: { options: APIOptions; - userContext: any; + userContext: UserContext; }): Promise<{ keys: JsonWebKey[] } | GeneralErrorResponse> { const resp = await options.recipeImplementation.getJWKS({ userContext }); diff --git a/lib/ts/recipe/jwt/index.ts b/lib/ts/recipe/jwt/index.ts index 7e3c39022..a2dd740df 100644 --- a/lib/ts/recipe/jwt/index.ts +++ b/lib/ts/recipe/jwt/index.ts @@ -13,24 +13,30 @@ * under the License. */ +import { getUserContext } from "../../utils"; import Recipe from "./recipe"; import { APIInterface, RecipeInterface, APIOptions, JsonWebKey } from "./types"; export default class Wrapper { static init = Recipe.init; - static async createJWT(payload: any, validitySeconds?: number, useStaticSigningKey?: boolean, userContext?: any) { + static async createJWT( + payload: any, + validitySeconds?: number, + useStaticSigningKey?: boolean, + userContext?: Record + ) { return await Recipe.getInstanceOrThrowError().recipeInterfaceImpl.createJWT({ payload, validitySeconds, useStaticSigningKey, - userContext: userContext === undefined ? {} : userContext, + userContext: getUserContext(userContext), }); } - static async getJWKS(userContext?: any) { + static async getJWKS(userContext?: Record) { return await Recipe.getInstanceOrThrowError().recipeInterfaceImpl.getJWKS({ - userContext: userContext === undefined ? {} : userContext, + userContext: getUserContext(userContext), }); } } diff --git a/lib/ts/recipe/jwt/recipe.ts b/lib/ts/recipe/jwt/recipe.ts index 52649ba72..80045cdc0 100644 --- a/lib/ts/recipe/jwt/recipe.ts +++ b/lib/ts/recipe/jwt/recipe.ts @@ -20,7 +20,7 @@ import NormalisedURLPath from "../../normalisedURLPath"; import normalisedURLPath from "../../normalisedURLPath"; import { Querier } from "../../querier"; import RecipeModule from "../../recipeModule"; -import { APIHandled, HTTPMethod, NormalisedAppinfo, RecipeListFunction } from "../../types"; +import { APIHandled, HTTPMethod, NormalisedAppinfo, RecipeListFunction, UserContext } from "../../types"; import getJWKS from "./api/getJWKS"; import APIImplementation from "./api/implementation"; import { GET_JWKS_API } from "./constants"; @@ -102,7 +102,7 @@ export default class Recipe extends RecipeModule { res: BaseResponse, _path: normalisedURLPath, _method: HTTPMethod, - userContext: any + userContext: UserContext ): Promise => { let options = { config: this.config, @@ -116,7 +116,7 @@ export default class Recipe extends RecipeModule { return await getJWKS(this.apiImpl, options, userContext); }; - handleError(error: error, _: BaseRequest, __: BaseResponse): Promise { + handleError(error: error, _: BaseRequest, __: BaseResponse, _userContext: UserContext): Promise { throw error; } diff --git a/lib/ts/recipe/jwt/recipeImplementation.ts b/lib/ts/recipe/jwt/recipeImplementation.ts index 97eebf111..dc8656124 100644 --- a/lib/ts/recipe/jwt/recipeImplementation.ts +++ b/lib/ts/recipe/jwt/recipeImplementation.ts @@ -15,7 +15,7 @@ import NormalisedURLPath from "../../normalisedURLPath"; import { Querier } from "../../querier"; -import { NormalisedAppinfo } from "../../types"; +import { NormalisedAppinfo, UserContext } from "../../types"; import { JsonWebKey, RecipeInterface, TypeNormalisedInput } from "./types"; const defaultJWKSMaxAge = 60; // This corresponds to the dynamicSigningKeyOverlapMS in the core @@ -35,7 +35,7 @@ export default function getRecipeInterface( payload?: any; useStaticSigningKey?: boolean; validitySeconds?: number; - userContext: any; + userContext: UserContext; }): Promise< | { status: "OK"; @@ -74,7 +74,7 @@ export default function getRecipeInterface( } }, - getJWKS: async function (userContext: any): Promise<{ keys: JsonWebKey[]; validityInSeconds?: number }> { + getJWKS: async function ({ userContext }): Promise<{ keys: JsonWebKey[]; validityInSeconds?: number }> { const { body, headers } = await querier.sendGetRequestWithResponseHeaders( new NormalisedURLPath("/.well-known/jwks.json"), {}, diff --git a/lib/ts/recipe/jwt/types.ts b/lib/ts/recipe/jwt/types.ts index 85453ff08..c241968df 100644 --- a/lib/ts/recipe/jwt/types.ts +++ b/lib/ts/recipe/jwt/types.ts @@ -15,7 +15,7 @@ import type { BaseRequest, BaseResponse } from "../../framework"; import OverrideableBuilder from "supertokens-js-override"; -import { GeneralErrorResponse } from "../../types"; +import { GeneralErrorResponse, UserContext } from "../../types"; export type JsonWebKey = { kty: string; @@ -62,7 +62,7 @@ export type RecipeInterface = { payload?: any; validitySeconds?: number; useStaticSigningKey?: boolean; - userContext: any; + userContext: UserContext; }): Promise< | { status: "OK"; @@ -74,7 +74,7 @@ export type RecipeInterface = { >; getJWKS(input: { - userContext: any; + userContext: UserContext; }): Promise<{ keys: JsonWebKey[]; validityInSeconds?: number; @@ -86,6 +86,6 @@ export type APIInterface = { | undefined | ((input: { options: APIOptions; - userContext: any; + userContext: UserContext; }) => Promise<{ keys: JsonWebKey[] } | GeneralErrorResponse>); }; diff --git a/lib/ts/recipe/multifactorauth/api/implementation.ts b/lib/ts/recipe/multifactorauth/api/implementation.ts index 1210d81e8..cee395551 100644 --- a/lib/ts/recipe/multifactorauth/api/implementation.ts +++ b/lib/ts/recipe/multifactorauth/api/implementation.ts @@ -1,5 +1,95 @@ +import Multitenancy from "../../multitenancy"; import { APIInterface } from "../"; +import { MultiFactorAuthClaim } from "../multiFactorAuthClaim"; +import { getUser } from "../../.."; +import SessionError from "../../session/error"; export default function getAPIInterface(): APIInterface { - return {} as any; + return { + mfaInfoGET: async ({ options, session, userContext }) => { + const userId = session.getUserId(); + const tenantId = session.getTenantId(); + const user = await getUser(userId, userContext); + + if (user === undefined) { + throw new SessionError({ + type: SessionError.UNAUTHORISED, + message: "Session user not found", + }); + } + const tenantInfo = await Multitenancy.getTenant(tenantId, userContext); + + const isAlreadySetup = await options.recipeImplementation.getFactorsSetupForUser({ + tenantId, + user, + userContext, + }); + + const { status: _, ...tenantConfig } = tenantInfo!; + + const availableFactors = await options.recipeInstance.getAllAvailableFactorIds(tenantConfig); + + // session is active and a new user is going to be created, so we need to check if the factor setup is allowed + const defaultRequiredFactorIdsForUser = await options.recipeImplementation.getDefaultRequiredFactorsForUser( + { + user: user!, + tenantId, + userContext, + } + ); + const completedFactorsClaimValue = await session.getClaimValue(MultiFactorAuthClaim, userContext); + const completedFactors = completedFactorsClaimValue?.c ?? {}; + const mfaRequirementsForAuth = await options.recipeImplementation.getMFARequirementsForAuth({ + user: user, + accessTokenPayload: session.getAccessTokenPayload(), + tenantId, + factorsSetUpForUser: isAlreadySetup, + defaultRequiredFactorIdsForTenant: tenantInfo?.defaultRequiredFactorIds ?? [], + defaultRequiredFactorIdsForUser, + completedFactors: completedFactors, + userContext, + }); + + const isAllowedToSetup = []; + for (const id of availableFactors) { + if ( + await options.recipeImplementation.isAllowedToSetupFactor({ + session, + factorId: id, + completedFactors: completedFactors, + defaultRequiredFactorIdsForTenant: tenantInfo?.defaultRequiredFactorIds ?? [], + defaultRequiredFactorIdsForUser, + factorsSetUpForUser: isAlreadySetup, + mfaRequirementsForAuth, + userContext, + }) + ) { + isAllowedToSetup.push(id); + } + } + + let selectedEmail = user.emails[0]; + + for (const loginMethod of user.loginMethods) { + if (loginMethod.recipeUserId.getAsString() === session.getRecipeUserId().getAsString()) { + if (loginMethod.email !== undefined) { + selectedEmail = loginMethod.email; + } + break; + } + } + + await session.fetchAndSetClaim(MultiFactorAuthClaim, userContext); + + return { + status: "OK", + factors: { + isAllowedToSetup, + isAlreadySetup, + }, + email: selectedEmail, + phoneNumber: user.phoneNumbers[0], + }; + }, + }; } diff --git a/lib/ts/recipe/multifactorauth/api/mfaInfo.ts b/lib/ts/recipe/multifactorauth/api/mfaInfo.ts index 00f987f9a..79f400e1d 100644 --- a/lib/ts/recipe/multifactorauth/api/mfaInfo.ts +++ b/lib/ts/recipe/multifactorauth/api/mfaInfo.ts @@ -16,14 +16,13 @@ import { send200Response } from "../../../utils"; import { APIInterface, APIOptions } from ".."; import Session from "../../session"; +import { UserContext } from "../../../types"; export default async function mfaInfo( apiImplementation: APIInterface, options: APIOptions, - userContext: any + userContext: UserContext ): Promise { - let result; - if (apiImplementation.mfaInfoGET === undefined) { return false; } @@ -40,14 +39,7 @@ export default async function mfaInfo( session, userContext, }); - if (response.status === "OK") { - // if there is a new session, it will be - // automatically added to the response by the createNewSession function call - // inside the verifyEmailPOST function. - result = { status: "OK" }; - } else { - result = response; - } - send200Response(options.res, result); + + send200Response(options.res, response); return true; } diff --git a/lib/ts/recipe/multifactorauth/constants.ts b/lib/ts/recipe/multifactorauth/constants.ts index af896e7d0..3f5a2a418 100644 --- a/lib/ts/recipe/multifactorauth/constants.ts +++ b/lib/ts/recipe/multifactorauth/constants.ts @@ -13,4 +13,4 @@ * under the License. */ -export const GET_MFA_INFO = "/mfa-info"; +export const GET_MFA_INFO = "/mfa/info"; diff --git a/lib/ts/recipe/multifactorauth/error.ts b/lib/ts/recipe/multifactorauth/error.ts deleted file mode 100644 index 1ca06cf12..000000000 --- a/lib/ts/recipe/multifactorauth/error.ts +++ /dev/null @@ -1,25 +0,0 @@ -/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. - * - * This software is licensed under the Apache License, Version 2.0 (the - * "License") as published by the Apache Software Foundation. - * - * You may not use this file except in compliance with the License. You may - * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ - -import STError from "../../error"; - -export default class SessionError extends STError { - constructor(options: { type: "BAD_INPUT_ERROR"; message: string }) { - super({ - ...options, - }); - this.fromRecipe = "multifactorauth"; - } -} diff --git a/lib/ts/recipe/multifactorauth/index.ts b/lib/ts/recipe/multifactorauth/index.ts index 6b2c09f10..4f86a264b 100644 --- a/lib/ts/recipe/multifactorauth/index.ts +++ b/lib/ts/recipe/multifactorauth/index.ts @@ -17,29 +17,36 @@ import Recipe from "./recipe"; import { RecipeInterface, APIOptions, APIInterface } from "./types"; import { MultiFactorAuthClaim } from "./multiFactorAuthClaim"; import { SessionContainerInterface } from "../session/types"; +import Multitenancy from "../multitenancy"; import { getUser } from "../.."; +import UserMetadataRecipe from "../usermetadata/recipe"; +import { getUserContext } from "../../utils"; export default class Wrapper { static init = Recipe.init; static MultiFactorAuthClaim = MultiFactorAuthClaim; - static async getFactorsSetUpByUser(tenantId: string, userId: string, userContext?: any) { - const ctx = userContext ?? {}; + static async getFactorsSetupForUser(tenantId: string, userId: string, userContext?: Record) { + const ctx = getUserContext(userContext); const user = await getUser(userId, ctx); if (!user) { throw new Error("UKNKNOWN_USER_ID"); } return Recipe.getInstanceOrThrowError().recipeInterfaceImpl.getFactorsSetupForUser({ - user, tenantId, + user, userContext: ctx, }); } - static async isAllowedToSetupFactor(session: SessionContainerInterface, factorId: string, userContext?: any) { - let ctx = userContext ?? {}; + static async isAllowedToSetupFactor( + session: SessionContainerInterface, + factorId: string, + userContext?: Record + ) { + let ctx = getUserContext(userContext); const user = await getUser(session.getUserId(), ctx); if (!user) { throw new Error("UKNKNOWN_USER_ID"); @@ -51,11 +58,21 @@ export default class Wrapper { }); const mfaClaimValue = await session.getClaimValue(MultiFactorAuthClaim, ctx); const completedFactors = mfaClaimValue?.c ?? {}; - const defaultMFARequirementsForUser: string[] = []; // TODO - const defaultMFARequirementsForTenant: string[] = []; // TODO + const defaultMFARequirementsForUser = await Recipe.getInstanceOrThrowError().recipeInterfaceImpl.getDefaultRequiredFactorsForUser( + { + user, + tenantId: session.getTenantId(), + userContext: ctx, + } + ); + + const tenantInfo = await Multitenancy.getTenant(session.getTenantId(), userContext); + const defaultMFARequirementsForTenant: string[] = tenantInfo?.defaultRequiredFactorIds ?? []; const requirements = await Recipe.getInstanceOrThrowError().recipeInterfaceImpl.getMFARequirementsForAuth({ - session, - factorsSetUpByTheUser: factorsSetup, + user, + accessTokenPayload: session.getAccessTokenPayload(), + tenantId: session.getTenantId(), + factorsSetUpForUser: factorsSetup, defaultRequiredFactorIdsForUser: defaultMFARequirementsForUser, defaultRequiredFactorIdsForTenant: defaultMFARequirementsForTenant, completedFactors, @@ -66,29 +83,66 @@ export default class Wrapper { factorId, completedFactors, mfaRequirementsForAuth: requirements, - factorsSetUpByTheUser: factorsSetup, + factorsSetUpForUser: factorsSetup, defaultRequiredFactorIdsForUser: defaultMFARequirementsForUser, defaultRequiredFactorIdsForTenant: defaultMFARequirementsForTenant, - userContext, + userContext: ctx, }); } static async markFactorAsCompleteInSession( session: SessionContainerInterface, factorId: string, - userContext?: any + userContext?: Record ) { return Recipe.getInstanceOrThrowError().recipeInterfaceImpl.markFactorAsCompleteInSession({ session, factorId, - userContext: userContext ?? {}, + userContext: getUserContext(userContext), + }); + } + + static async addToDefaultRequiredFactorsForUser( + userId: string, + factorId: string, + userContext?: Record + ) { + const ctx = getUserContext(userContext); + const userMetadataInstance = UserMetadataRecipe.getInstanceOrThrowError(); + const metadata = await userMetadataInstance.recipeInterfaceImpl.getUserMetadata({ + userId, + userContext: ctx, + }); + + const factorIds = metadata.metadata._supertokens?.defaultRequiredFactorIdsForUser ?? []; + if (factorIds.includes(factorId)) { + return; + } + + factorIds.push(factorId); + + const metadataUpdate = { + ...metadata.metadata, + _supertokens: { + ...metadata.metadata._supertokens, + defaultRequiredFactorIdsForUser: factorIds, + }, + }; + + await userMetadataInstance.recipeInterfaceImpl.updateUserMetadataInternal({ + userId: userId, + metadataUpdate, + userContext: ctx, }); } } export let init = Wrapper.init; +export let getFactorsSetupForUser = Wrapper.getFactorsSetupForUser; +export let isAllowedToSetupFactor = Wrapper.isAllowedToSetupFactor; export let markFactorAsCompleteInSession = Wrapper.markFactorAsCompleteInSession; +export const addToDefaultRequiredFactorsForUser = Wrapper.addToDefaultRequiredFactorsForUser; export { MultiFactorAuthClaim }; export type { RecipeInterface, APIOptions, APIInterface }; diff --git a/lib/ts/recipe/multifactorauth/multiFactorAuthClaim.ts b/lib/ts/recipe/multifactorauth/multiFactorAuthClaim.ts index 3176ae924..b2503ed34 100644 --- a/lib/ts/recipe/multifactorauth/multiFactorAuthClaim.ts +++ b/lib/ts/recipe/multifactorauth/multiFactorAuthClaim.ts @@ -1,39 +1,217 @@ +import { getUser } from "../.."; import RecipeUserId from "../../recipeUserId"; import { SessionClaimValidator } from "../session"; import { SessionClaim } from "../session/claims"; import { JSONObject } from "../usermetadata"; import { MFAClaimValue, MFARequirementList } from "./types"; +import Multitenancy from "../multitenancy"; +import MultiFactorAuthRecipe from "./recipe"; +import { DEFAULT_TENANT_ID } from "../multitenancy/constants"; +import { checkFactorRequirement } from "./utils"; +import { UserContext } from "../../types"; /** * We include "Class" in the class name, because it makes it easier to import the right thing (the instance) instead of this. * */ export class MultiFactorAuthClaimClass extends SessionClaim { - public validators: { - passesMFARequirements: (requirements?: MFARequirementList) => SessionClaimValidator; - }; constructor(key?: string) { super(key ?? "st-mfa"); this.validators = { - passesMFARequirements: (_requirements?: MFARequirementList) => ({} as SessionClaimValidator), // TODO + hasCompletedDefaultFactors: (id?: string) => ({ + claim: this, + id: id ?? this.key, + + shouldRefetch: (payload) => { + const value = this.getValueFromPayload(payload); + return value === undefined; + }, + validate: async (payload) => { + const claimVal = this.getValueFromPayload(payload); + if (claimVal === undefined) { + throw new Error("This should never happen, claim value not present in payload"); + } + + const { n } = claimVal; + + if (n.length === 0) { + return { + isValid: true, + }; + } + + return { + isValid: false, + reason: { + message: "not all required factors have been completed", + nextFactorOptions: n, + }, + }; + }, + }), + + hasCompletedFactors: (requirements: MFARequirementList, id?: string) => ({ + claim: this, + id: id ?? this.key, + + shouldRefetch: (payload) => { + const value = this.getValueFromPayload(payload); + return value === undefined; + }, + validate: async (payload) => { + if (requirements.length === 0) { + return { + isValid: true, // No requirements to satisfy + }; + } + + const claimVal = this.getValueFromPayload(payload); + if (claimVal === undefined) { + throw new Error("This should never happen, claim value not present in payload"); + } + + const { c } = claimVal; + + for (const req of requirements) { + if (typeof req === "object" && "oneOf" in req) { + const res = req.oneOf + .map((r) => checkFactorRequirement(r, c)) + .filter((v) => v.isValid === false); + if (res.length === req.oneOf.length) { + return { + isValid: false, + reason: { + message: "All factor checkers failed in the list", + oneOf: req.oneOf, + failures: res, + }, + }; + } + } else if (typeof req === "object" && "allOf" in req) { + const res = req.allOf + .map((r) => checkFactorRequirement(r, c)) + .filter((v) => v.isValid === false); + if (res.length !== 0) { + return { + isValid: false, + reason: { + message: "Some factor checkers failed in the list", + allOf: req.allOf, + failures: res, + }, + }; + } + } else { + const res = checkFactorRequirement(req, c); + if (res.isValid !== true) { + return { + isValid: false, + reason: { + message: "Factor validation failed: " + res.message, + factorId: res.id, + }, + }; + } + } + } + + return { + isValid: true, + }; + }, + }), }; } - public buildNextArray(_completedClaims: MFAClaimValue["c"], _requirements: MFARequirementList) { - // TODO + public validators: { + hasCompletedDefaultFactors: (id?: string) => SessionClaimValidator; + hasCompletedFactors(requirements: MFARequirementList, id?: string): SessionClaimValidator; + }; + + public buildNextArray(completedClaims: MFAClaimValue["c"], requirements: MFARequirementList): string[] { + for (const req of requirements) { + const nextFactors: Set = new Set(); + + if (typeof req === "string") { + if (completedClaims[req] === undefined) { + nextFactors.add(req); + } + } else if ("oneOf" in req) { + let satisfied = false; + for (const factorId of req.oneOf) { + if (completedClaims[factorId] !== undefined) { + satisfied = true; + } + } + if (!satisfied) { + for (const factorId of req.oneOf) { + nextFactors.add(factorId); + } + } + } else if ("allOf" in req) { + for (const factorId of req.allOf) { + if (completedClaims[factorId] === undefined) { + nextFactors.add(factorId); + } + } + } + if (nextFactors.size > 0) { + return Array.from(nextFactors); + } + } return []; } - public fetchValue = ( - _userId: string, + public fetchValue = async ( + userId: string, _recipeUserId: RecipeUserId, - _tenantId: string | undefined, - _userContext: any + tenantId: string | undefined, + currentPayload: JSONObject | undefined, + userContext: UserContext ) => { - // TODO + const user = await getUser(userId, userContext); + + if (user === undefined) { + throw new Error("Unknown User ID provided"); + } + const tenantInfo = await Multitenancy.getTenant(tenantId ?? DEFAULT_TENANT_ID, userContext); + + if (tenantInfo === undefined) { + throw new Error("should never happen"); + } + + const recipeInstance = MultiFactorAuthRecipe.getInstanceOrThrowError(); + const isAlreadySetup = await recipeInstance.recipeInterfaceImpl.getFactorsSetupForUser({ + user, + tenantId: tenantId ?? DEFAULT_TENANT_ID, + userContext, + }); + + // session is active and a new user is going to be created, so we need to check if the factor setup is allowed + const defaultRequiredFactorIdsForUser = await recipeInstance.recipeInterfaceImpl.getDefaultRequiredFactorsForUser( + { + user: user!, + tenantId: tenantId ?? DEFAULT_TENANT_ID, + userContext, + } + ); + const completedFactorsClaimValue = currentPayload && (currentPayload[this.key] as JSONObject); + const completedFactors: Record = + (completedFactorsClaimValue?.c as Record) ?? {}; + const mfaRequirementsForAuth = await recipeInstance.recipeInterfaceImpl.getMFARequirementsForAuth({ + user, + accessTokenPayload: currentPayload !== undefined ? currentPayload : {}, + tenantId: tenantId ?? DEFAULT_TENANT_ID, + factorsSetUpForUser: isAlreadySetup, + defaultRequiredFactorIdsForTenant: tenantInfo?.defaultRequiredFactorIds ?? [], + defaultRequiredFactorIdsForUser, + completedFactors: completedFactors, + userContext, + }); + return { - c: {}, - n: [], + c: completedFactors, + n: MultiFactorAuthClaim.buildNextArray(completedFactors, mfaRequirementsForAuth), }; }; diff --git a/lib/ts/recipe/multifactorauth/recipe.ts b/lib/ts/recipe/multifactorauth/recipe.ts index f67fc8757..0adff5caa 100644 --- a/lib/ts/recipe/multifactorauth/recipe.ts +++ b/lib/ts/recipe/multifactorauth/recipe.ts @@ -16,25 +16,44 @@ import OverrideableBuilder from "supertokens-js-override"; import { BaseRequest, BaseResponse } from "../../framework"; import NormalisedURLPath from "../../normalisedURLPath"; -import { Querier } from "../../querier"; import RecipeModule from "../../recipeModule"; import STError from "../../error"; -import { APIHandled, HTTPMethod, NormalisedAppinfo, RecipeListFunction } from "../../types"; +import { APIHandled, HTTPMethod, NormalisedAppinfo, RecipeListFunction, UserContext } from "../../types"; import RecipeImplementation from "./recipeImplementation"; import APIImplementation from "./api/implementation"; -import { ProviderInput } from "../thirdparty/types"; import { GET_MFA_INFO } from "./constants"; import { MultiFactorAuthClaim } from "./multiFactorAuthClaim"; -import { APIInterface, RecipeInterface, TypeInput, TypeNormalisedInput } from "./types"; +import { + APIInterface, + GetAllFactorsFromOtherRecipesFunc, + GetFactorsSetupForUserFromOtherRecipesFunc, + MFAFlowErrors, + RecipeInterface, + TypeInput, + TypeNormalisedInput, +} from "./types"; import { validateAndNormaliseUserInput } from "./utils"; import mfaInfoAPI from "./api/mfaInfo"; import SessionRecipe from "../session/recipe"; import { PostSuperTokensInitCallbacks } from "../../postSuperTokensInitCallbacks"; +import { User } from "../../user"; +import { SessionContainerInterface } from "../session/types"; +import RecipeUserId from "../../recipeUserId"; +import Multitenancy from "../multitenancy"; +import Session from "../session"; +import AccountLinkingRecipe from "../accountlinking/recipe"; +import { getUser, listUsersByAccountInfo } from "../.."; +import { Querier } from "../../querier"; +import SessionError from "../session/error"; +import { TenantConfig } from "../multitenancy/types"; export default class Recipe extends RecipeModule { private static instance: Recipe | undefined = undefined; static RECIPE_ID = "multifactorauth"; + getFactorsSetupForUserFromOtherRecipesFuncs: GetFactorsSetupForUserFromOtherRecipesFunc[] = []; + getAllFactorsFromOtherRecipesFunc: GetAllFactorsFromOtherRecipesFunc[] = []; + config: TypeNormalisedInput; recipeInterfaceImpl: RecipeInterface; @@ -43,9 +62,7 @@ export default class Recipe extends RecipeModule { isInServerlessEnv: boolean; - staticThirdPartyProviders: ProviderInput[] = []; - - getAllowedDomainsForTenantId?: (tenantId: string, userContext: any) => Promise; + querier: Querier; constructor(recipeId: string, appInfo: NormalisedAppinfo, isInServerlessEnv: boolean, config?: TypeInput) { super(recipeId, appInfo); @@ -53,7 +70,7 @@ export default class Recipe extends RecipeModule { this.isInServerlessEnv = isInServerlessEnv; { - let builder = new OverrideableBuilder(RecipeImplementation(Querier.getNewInstanceOrThrowError(recipeId))); + let builder = new OverrideableBuilder(RecipeImplementation(this)); this.recipeInterfaceImpl = builder.override(this.config.override.functions).build(); } @@ -61,6 +78,8 @@ export default class Recipe extends RecipeModule { let builder = new OverrideableBuilder(APIImplementation()); this.apiImpl = builder.override(this.config.override.apis).build(); } + + this.querier = Querier.getNewInstanceOrThrowError(recipeId); } static getInstanceOrThrowError(): Recipe { @@ -84,7 +103,7 @@ export default class Recipe extends RecipeModule { PostSuperTokensInitCallbacks.addPostInitCallback(() => { SessionRecipe.getInstanceOrThrowError().addClaimValidatorFromOtherRecipe( - MultiFactorAuthClaim.validators.passesMFARequirements() + MultiFactorAuthClaim.validators.hasCompletedDefaultFactors() ); }); return Recipe.instance; @@ -123,16 +142,16 @@ export default class Recipe extends RecipeModule { res: BaseResponse, _: NormalisedURLPath, __: HTTPMethod, - userContext: any + userContext: UserContext ): Promise => { let options = { + recipeInstance: this, recipeImplementation: this.recipeInterfaceImpl, config: this.config, recipeId: this.getRecipeId(), isInServerlessEnv: this.isInServerlessEnv, req, res, - staticThirdPartyProviders: this.staticThirdPartyProviders, }; if (id === GET_MFA_INFO) { return await mfaInfoAPI(this.apiImpl, options, userContext); @@ -151,4 +170,351 @@ export default class Recipe extends RecipeModule { isErrorFromThisRecipe = (err: any): err is STError => { return STError.isErrorFromSuperTokens(err) && err.fromRecipe === Recipe.RECIPE_ID; }; + + addGetAllFactorsFromOtherRecipesFunc = (f: GetAllFactorsFromOtherRecipesFunc) => { + this.getAllFactorsFromOtherRecipesFunc.push(f); + }; + + getAllAvailableFactorIds = (tenantConfig: TenantConfig) => { + let factorIds: string[] = []; + for (const func of this.getAllFactorsFromOtherRecipesFunc) { + const factorIdsRes = func(tenantConfig); + factorIds = factorIds.concat(factorIdsRes.factorIds); + } + return factorIds; + }; + + getAllAvailableFirstFactorIds = (tenantConfig: TenantConfig) => { + let factorIds: string[] = []; + for (const func of this.getAllFactorsFromOtherRecipesFunc) { + const factorIdsRes = func(tenantConfig); + factorIds = factorIds.concat(factorIdsRes.firstFactorIds); + } + return factorIds; + }; + + addGetFactorsSetupForUserFromOtherRecipes = (func: GetFactorsSetupForUserFromOtherRecipesFunc) => { + this.getFactorsSetupForUserFromOtherRecipesFuncs.push(func); + }; + + validateForMultifactorAuthBeforeFactorCompletion = async ({ + tenantId, + factorIdInProgress, + session, + userLoggingIn, + isAlreadySetup, + signUpInfo, + userContext, + }: { + req: BaseRequest; + res: BaseResponse; + tenantId: string; + factorIdInProgress: string; + session?: SessionContainerInterface; + userLoggingIn?: User; + isAlreadySetup?: boolean; + signUpInfo?: { + email: string; + isVerifiedFactor: boolean; + }; + userContext: UserContext; + }): Promise<{ status: "OK" } | MFAFlowErrors> => { + const tenantInfo = await Multitenancy.getTenant(tenantId, userContext); + const { status: _, ...tenantConfig } = tenantInfo!; + const validFirstFactors = + tenantInfo?.firstFactors || this.config.firstFactors || this.getAllAvailableFirstFactorIds(tenantConfig); + + if (session === undefined) { + // No session exists, so we need to check if it's a valid first factor before proceeding + if (!validFirstFactors.includes(factorIdInProgress)) { + return { + status: "DISALLOWED_FIRST_FACTOR_ERROR", + }; + } + return { + status: "OK", + }; + } + + let sessionUser; + if (userLoggingIn) { + if (userLoggingIn.id !== session.getUserId()) { + // the user trying to login is not linked to the session user, based on session behaviour + // we just return OK and do nothing or replace replace the existing session with a new one + // we are doing this because we allow factor setup only when creating a new user + + // this can happen when you got into login screen with an existing session and tried to log in with a different credentials + // or a case while doing secondary factor for phone otp but the user created a different account with the same phone number + return { + status: "OK", + }; + } + sessionUser = userLoggingIn; + } else { + sessionUser = await getUser(session.getUserId(), userContext); + } + + if (!sessionUser) { + // Session user doesn't exist, maybe the user was deleted + // Race condition, user got deleted in parallel, throw unauthorized + throw new SessionError({ + type: SessionError.UNAUTHORISED, + message: "Session user not found", + }); + } + + if (isAlreadySetup) { + return { + status: "OK", + }; + } + + // Check if the new user being created can be linked via MFA on the following conditions: + // 1. the new factor is a verified factor + // 2. the session user has a login method with same email and is verified + if (signUpInfo !== undefined) { + if (!signUpInfo.isVerifiedFactor) { + /* + We discussed another method but did not go ahead with it, details below: + + We can allow the second factor to be linked to first factor even if the emails are different + and not verified as long as there is no other user that exists (recipe or primary) that has the + same email as that of the second factor. For example, if first factor is google login with e1 + and second factor is email password with e2, we allow linking them as long as there is no other + user with email e2. + + We rejected this idea cause if auto account linking is switched off, then someone else can sign up + with google using e2. This is OK as it would not link (since account linking is switched off). + But, then if account linking is switched on, then the google sign in (and not sign up) with e2 + would actually cause it to be linked with the e1 account. + */ + + let foundVerifiedEmail = false; + for (const lM of sessionUser?.loginMethods) { + if (lM.email === signUpInfo.email && lM.verified) { + foundVerifiedEmail = true; + break; + } + } + if (!foundVerifiedEmail) { + return { + status: "FACTOR_SETUP_NOT_ALLOWED_ERROR", + message: "Cannot setup factor as the email is not verified", + }; + } + } + + // Check if there if the linking with session user going to fail and avoid user creation here + const users = await listUsersByAccountInfo(tenantId, { email: signUpInfo.email }, undefined, userContext); + for (const user of users) { + if (user.isPrimaryUser && user.id !== sessionUser.id) { + return { + status: "ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR", + message: + "Cannot setup factor as the email is already associated with another primary user. Please contact support. (ERR_CODE_012)", + }; + } + } + } + + // session is active and a new user is going to be created, so we need to check if the factor setup is allowed + const defaultRequiredFactorIdsForUser = await this.recipeInterfaceImpl.getDefaultRequiredFactorsForUser({ + user: sessionUser, + tenantId, + userContext, + }); + const factorsSetUpForUser = await this.recipeInterfaceImpl.getFactorsSetupForUser({ + user: sessionUser, + tenantId, + userContext, + }); + const completedFactorsClaimValue = await session.getClaimValue(MultiFactorAuthClaim, userContext); + const mfaRequirementsForAuth = await this.recipeInterfaceImpl.getMFARequirementsForAuth({ + user: sessionUser, + accessTokenPayload: session.getAccessTokenPayload(), + tenantId, + factorsSetUpForUser, + defaultRequiredFactorIdsForTenant: tenantInfo?.defaultRequiredFactorIds ?? [], + defaultRequiredFactorIdsForUser, + completedFactors: completedFactorsClaimValue?.c ?? {}, + userContext, + }); + + const canSetup = await this.recipeInterfaceImpl.isAllowedToSetupFactor({ + session, + factorId: factorIdInProgress, + completedFactors: completedFactorsClaimValue?.c ?? {}, + defaultRequiredFactorIdsForTenant: tenantInfo?.defaultRequiredFactorIds ?? [], + defaultRequiredFactorIdsForUser, + factorsSetUpForUser, + mfaRequirementsForAuth, + userContext, + }); + if (!canSetup) { + return { + status: "FACTOR_SETUP_NOT_ALLOWED_ERROR", + }; + } + + return { + status: "OK", + }; + }; + + createOrUpdateSessionForMultifactorAuthAfterFactorCompletion = async ({ + req, + res, + tenantId, + factorIdInProgress, + justCompletedFactorUserInfo, + userContext, + }: { + req: BaseRequest; + res: BaseResponse; + tenantId: string; + factorIdInProgress: string; + isAlreadySetup?: boolean; + justCompletedFactorUserInfo?: { + user: User; + createdNewUser: boolean; + recipeUserId: RecipeUserId; + }; + userContext: UserContext; + }): Promise< + | { + status: "OK"; + session: SessionContainerInterface; + } + | MFAFlowErrors + > => { + let session = await Session.getSession(req, res, { + sessionRequired: false, + overrideGlobalClaimValidators: () => [], + }); + if ( + session === undefined // no session exists, so we can create a new one + ) { + if (justCompletedFactorUserInfo === undefined) { + throw new Error("should never come here"); // We wouldn't create new session from a recipe like TOTP + } + + const newSession = await Session.createNewSession( + req, + res, + tenantId, + justCompletedFactorUserInfo.recipeUserId, + {}, + {}, + userContext + ); + await this.recipeInterfaceImpl.markFactorAsCompleteInSession({ + session: newSession, + factorId: factorIdInProgress, + userContext, + }); + return { + status: "OK", + session: newSession, + }; + } + + while (true) { + // loop to handle race conditions + const sessionUser = await getUser(session.getUserId(), userContext); + + // race condition, user deleted throw unauthorized + if (sessionUser === undefined) { + throw new SessionError({ + type: SessionError.UNAUTHORISED, + message: "Session user not found", + }); + } + + if (justCompletedFactorUserInfo !== undefined) { + if (justCompletedFactorUserInfo.createdNewUser) { + // This is a newly created user, so it must be account linked with the session user + if (!sessionUser.isPrimaryUser) { + const createPrimaryRes = await AccountLinkingRecipe.getInstance().recipeInterfaceImpl.createPrimaryUser( + { + recipeUserId: new RecipeUserId(sessionUser.id), + userContext, + } + ); + if (createPrimaryRes.status === "RECIPE_USER_ID_ALREADY_LINKED_WITH_PRIMARY_USER_ID_ERROR") { + // Race condition + this.querier.invalidateCoreCallCache(userContext); + continue; + } else if ( + createPrimaryRes.status === + "ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR" + ) { + return { + status: "ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR", + message: "Error setting up MFA for the user. Please contact support. (ERR_CODE_009)", + }; + } + } + + const linkRes = await AccountLinkingRecipe.getInstance().recipeInterfaceImpl.linkAccounts({ + recipeUserId: justCompletedFactorUserInfo.recipeUserId, + primaryUserId: sessionUser.id, + userContext, + }); + + if (linkRes.status === "RECIPE_USER_ID_ALREADY_LINKED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR") { + return { + status: "RECIPE_USER_ID_ALREADY_LINKED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR", + message: + "Error setting up MFA for the user because of the automatic account linking. Please contact support. (ERR_CODE_011)", + }; + } else if (linkRes.status === "INPUT_USER_IS_NOT_A_PRIMARY_USER") { + // Race condition + this.querier.invalidateCoreCallCache(userContext); + continue; + } else if ( + linkRes.status === "ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR" + ) { + return { + status: "ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR", + message: + "Cannot complete factor setup as the account info is already associated with another primary user. Please contact support. (ERR_CODE_010)", + }; + } + } else { + // Not a new user we should check if the user is linked to the session user + const loggedInUserLinkedToSessionUser = sessionUser.id === justCompletedFactorUserInfo.user.id; + if (!loggedInUserLinkedToSessionUser) { + // we may keep or replace the session as per the flag overwriteSessionDuringSignIn in session recipe + session = await Session.createNewOrKeepExistingSession( + req, + res, + tenantId, + justCompletedFactorUserInfo.recipeUserId, + {}, + {}, + userContext + ); + + return { + status: "OK", + session: session, + }; + } + } + } + + break; + } + + await this.recipeInterfaceImpl.markFactorAsCompleteInSession({ + session: session, + factorId: factorIdInProgress, + userContext, + }); + + return { + status: "OK", + session: session, + }; + }; } diff --git a/lib/ts/recipe/multifactorauth/recipeImplementation.ts b/lib/ts/recipe/multifactorauth/recipeImplementation.ts index f7215c52f..02f651794 100644 --- a/lib/ts/recipe/multifactorauth/recipeImplementation.ts +++ b/lib/ts/recipe/multifactorauth/recipeImplementation.ts @@ -1,152 +1,159 @@ import { RecipeInterface } from "./"; -import { Querier } from "../../querier"; -import RecipeUserId from "../../recipeUserId"; -import { User } from "../../user"; -import NormalisedURLPath from "../../normalisedURLPath"; +import UserMetadataRecipe from "../usermetadata/recipe"; +import { MultiFactorAuthClaim } from "./multiFactorAuthClaim"; +import type MultiFactorAuthRecipe from "./recipe"; +import Multitenancy from "../multitenancy"; +import { getUser } from "../.."; +import { logDebugMessage } from "../../logger"; -export default function getRecipeInterface(querier: Querier): RecipeInterface { +export default function getRecipeInterface(recipeInstance: MultiFactorAuthRecipe): RecipeInterface { return { - // markFactorAsCompleteInSession: async ({ session, factor, userContext }) => { - // const currentValue = await session.getClaimValue(MultiFactorAuthClaim); - // const completed = { - // ...currentValue?.c, - // [factor]: Math.floor(Date.now() / 1000), - // }; - // const setupUserFactors = await this.recipeInterfaceImpl.getFactorsSetupForUser({ - // userId: session.getUserId(), - // tenantId: session.getTenantId(), - // userContext, - // }); - // const requirements = await this.config.getMFARequirementsForAuth( - // session, - // setupUserFactors, - // completed, - // userContext - // ); - // const next = MultiFactorAuthClaim.buildNextArray(completed, requirements); - // await session.setClaimValue(MultiFactorAuthClaim, { - // c: completed, - // n: next, - // }); - // }, - - createPrimaryUser: async function ( - this: RecipeInterface, - { - recipeUserId, - userContext, - }: { - recipeUserId: RecipeUserId; - userContext: any; + getFactorsSetupForUser: async function ({ tenantId, user, userContext }) { + const tenantInfo = await Multitenancy.getTenant(tenantId, userContext); + if (tenantInfo === undefined) { + throw new Error("should never happen"); } - ): Promise< - | { - status: "OK"; - user: User; - wasAlreadyAPrimaryUser: boolean; - } - | { - status: "RECIPE_USER_ID_ALREADY_LINKED_WITH_PRIMARY_USER_ID_ERROR"; - primaryUserId: string; - } - | { - status: "ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR"; - primaryUserId: string; - description: string; - } - > { - let response = await querier.sendPostRequest( - new NormalisedURLPath("/recipe/mfa/user/primary"), - { - recipeUserId: recipeUserId.getAsString(), - }, - userContext - ); - if (response.status === "OK") { - response.user = new User(response.user); + let { status: _, ...tenantConfig } = tenantInfo; + let factorIds: string[] = []; + + for (const func of recipeInstance.getFactorsSetupForUserFromOtherRecipesFuncs) { + let result = await func(user, tenantConfig, userContext); + if (result !== undefined) { + factorIds = factorIds.concat(result); + } } - return response; + return factorIds; }, - linkAccounts: async function ( + getMFARequirementsForAuth: async function ({ + defaultRequiredFactorIdsForUser, + defaultRequiredFactorIdsForTenant, + completedFactors, + }) { + const loginTime = Math.min(...Object.values(completedFactors)); + const oldestFactor = Object.keys(completedFactors).find((k) => completedFactors[k] === loginTime); + const allFactors: Set = new Set(); + for (const factor of defaultRequiredFactorIdsForUser) { + allFactors.add(factor); + } + for (const factor of defaultRequiredFactorIdsForTenant) { + allFactors.add(factor); + } + /* + We are removing only oldestFactor but not all factors considering the case below: + Assume a user has emailpassword as first factor, and otp-phone & totp as secondary factors. + + once the the user logs in with emailpassword, that's added to the completedFactors array. + Then user completes let's say otp-phone, that's added as well. + + Now when we try to build the next array, this function is called and we must return + { oneOf: ['otp-phone', 'totp'] }, so that the auth is assumed complete for the default opt-in 2FA behaviour. + + If we remove all completed factors and return { oneOf: ['totp' ]} at this point, this will force the + user to complete totp as well, which will result in a 3FA. which we don't intend to do by default. + */ + allFactors.delete(oldestFactor!); // Removing the first factor if it exists + + return [{ oneOf: [...allFactors] }]; + }, + + isAllowedToSetupFactor: async function ( this: RecipeInterface, - { - recipeUserId, - primaryUserId, - userContext, - }: { - recipeUserId: RecipeUserId; - primaryUserId: string; - userContext: any; + { factorId, session, factorsSetUpForUser, userContext } + ) { + const claimVal = await session.getClaimValue(MultiFactorAuthClaim, userContext); + if (!claimVal) { + throw new Error("should never happen"); } - ): Promise< - | { - status: "OK"; - accountsAlreadyLinked: boolean; - user: User; - } - | { - status: "RECIPE_USER_ID_ALREADY_LINKED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR"; - user: User; - primaryUserId: string; - } - | { - status: "ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR"; - primaryUserId: string; - description: string; - } - | { - status: "INPUT_USER_IS_NOT_A_PRIMARY_USER"; - } - > { - const accountsLinkingResult = await querier.sendPostRequest( - new NormalisedURLPath("/recipe/accountlinking/user/link"), - { - recipeUserId: recipeUserId.getAsString(), - primaryUserId, - }, - userContext + + // // This solution: checks for 2FA (we'd allow factor setup if the user has set up only 1 factor group or completed at least 2) + // const factorGroups = [ + // ["otp-phone", "link-phone"], + // ["otp-email", "link-email"], + // ["emailpassword"], + // ["thirdparty"], + // ]; + // const setUpGroups = Array.from( + // new Set(factorsSetUpForUser.map((id) => factorGroups.find((f) => f.includes(id)) || [id])) + // ); + + // const completedGroups = setUpGroups.filter((group) => group.some((id) => claimVal.c[id] !== undefined)); + + // // If the user completed every factor they could + // if (setUpGroups.length === completedGroups.length) { + // logDebugMessage( + // `isAllowedToSetupFactor ${factorId}: true because the user completed all factors they have set up and this is required` + // ); + // return true; + // } + + // return completedGroups.length >= 2; + + if (claimVal.n.some((id) => factorsSetUpForUser.includes(id))) { + logDebugMessage( + `isAllowedToSetupFactor ${factorId}: false because there are items already set up in the next array: ${claimVal.n.join( + ", " + )}` + ); + return false; + } + logDebugMessage( + `isAllowedToSetupFactor ${factorId}: true because the next array is ${ + claimVal.n.length === 0 ? "empty" : "cannot be completed otherwise" + }` ); + return true; + }, - if ( - ["OK", "RECIPE_USER_ID_ALREADY_LINKED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR"].includes( - accountsLinkingResult.status - ) - ) { - accountsLinkingResult.user = new User(accountsLinkingResult.user); + markFactorAsCompleteInSession: async function (this: RecipeInterface, { session, factorId, userContext }) { + const currentValue = await session.getClaimValue(MultiFactorAuthClaim); + const completed = { + ...currentValue?.c, + [factorId]: Math.floor(Date.now() / 1000), + }; + const tenantId = session.getTenantId(); + const user = await getUser(session.getUserId(), userContext); + if (user === undefined) { + throw new Error("User not found!"); } - // TODO check if the code below is required - // if (accountsLinkingResult.status === "OK") { - // let user: UserType = accountsLinkingResult.user; - // if (!accountsLinkingResult.accountsAlreadyLinked) { - // await recipeInstance.verifyEmailForRecipeUserIfLinkedAccountsAreVerified({ - // user: user, - // recipeUserId, - // userContext, - // }); - - // const updatedUser = await this.getUser({ - // userId: primaryUserId, - // userContext, - // }); - // if (updatedUser === undefined) { - // throw Error("this error should never be thrown"); - // } - // user = updatedUser; - // let loginMethodInfo = user.loginMethods.find( - // (u) => u.recipeUserId.getAsString() === recipeUserId.getAsString() - // ); - // if (loginMethodInfo === undefined) { - // throw Error("this error should never be thrown"); - // } - - // // await config.onAccountLinked(user, loginMethodInfo, userContext); - // } - // accountsLinkingResult.user = user; - // } + const tenantInfo = await Multitenancy.getTenant(tenantId, userContext); + + const defaultRequiredFactorIdsForUser = await this.getDefaultRequiredFactorsForUser({ + user: user!, + tenantId, + userContext, + }); + const factorsSetUpForUser = await this.getFactorsSetupForUser({ + user: user!, + tenantId, + userContext, + }); + const mfaRequirementsForAuth = await this.getMFARequirementsForAuth({ + user, + accessTokenPayload: session.getAccessTokenPayload(), + tenantId, + factorsSetUpForUser, + defaultRequiredFactorIdsForTenant: tenantInfo?.defaultRequiredFactorIds ?? [], + defaultRequiredFactorIdsForUser, + completedFactors: completed, + userContext, + }); + const next = MultiFactorAuthClaim.buildNextArray(completed, mfaRequirementsForAuth); + await session.setClaimValue(MultiFactorAuthClaim, { + c: completed, + n: next, + }); + }, + + getDefaultRequiredFactorsForUser: async function ({ user, userContext }) { + const userMetadataInstance = UserMetadataRecipe.getInstanceOrThrowError(); + const metadata = await userMetadataInstance.recipeInterfaceImpl.getUserMetadata({ + userId: user.id, + userContext, + }); - return accountsLinkingResult; + return metadata.metadata._supertokens?.defaultRequiredFactorIdsForUser ?? []; }, - } as any; + }; } diff --git a/lib/ts/recipe/multifactorauth/types.ts b/lib/ts/recipe/multifactorauth/types.ts index f9672df18..6a0ffe641 100644 --- a/lib/ts/recipe/multifactorauth/types.ts +++ b/lib/ts/recipe/multifactorauth/types.ts @@ -15,10 +15,12 @@ import { BaseRequest, BaseResponse } from "../../framework"; import OverrideableBuilder from "supertokens-js-override"; -import { GeneralErrorResponse, User } from "../../types"; +import { GeneralErrorResponse, JSONObject, UserContext } from "../../types"; +import { User } from "../../user"; import { SessionContainer } from "../session"; import { SessionContainerInterface } from "../session/types"; -import RecipeUserId from "../../recipeUserId"; +import Recipe from "./recipe"; +import { TenantConfig } from "../multitenancy/types"; export type MFARequirementList = ( | { @@ -35,6 +37,15 @@ export type MFAClaimValue = { n: string[]; }; +export type MFAFlowErrors = { + status: + | "DISALLOWED_FIRST_FACTOR_ERROR" + | "FACTOR_SETUP_NOT_ALLOWED_ERROR" + | "ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR" + | "RECIPE_USER_ID_ALREADY_LINKED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR"; + message?: string; +}; + export type TypeInput = { firstFactors?: string[]; @@ -64,78 +75,42 @@ export type RecipeInterface = { session: SessionContainer; factorId: string; mfaRequirementsForAuth: MFARequirementList; - factorsSetUpByTheUser: string[]; + factorsSetUpForUser: string[]; defaultRequiredFactorIdsForUser: string[]; defaultRequiredFactorIdsForTenant: string[]; completedFactors: Record; - userContext: any; + userContext: UserContext; }) => Promise; getMFARequirementsForAuth: (input: { - session: SessionContainer; - factorsSetUpByTheUser: string[]; + user: User; + accessTokenPayload: JSONObject; + tenantId: string; + factorsSetUpForUser: string[]; defaultRequiredFactorIdsForUser: string[]; defaultRequiredFactorIdsForTenant: string[]; completedFactors: Record; - userContext: any; + userContext: UserContext; }) => Promise | MFARequirementList; markFactorAsCompleteInSession: (input: { session: SessionContainerInterface; factorId: string; - userContext?: any; + userContext: UserContext; }) => Promise; - getFactorsSetupForUser: (input: { user: User; tenantId: string; userContext: any }) => Promise; - - createPrimaryUser: (input: { - recipeUserId: RecipeUserId; - userContext: any; - }) => Promise< - | { - status: "OK"; - user: User; - wasAlreadyAPrimaryUser: boolean; - } - | { - status: "RECIPE_USER_ID_ALREADY_LINKED_WITH_PRIMARY_USER_ID_ERROR"; - primaryUserId: string; - } - | { - status: "ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR"; - primaryUserId: string; - description: string; - } - >; + getFactorsSetupForUser: (input: { tenantId: string; user: User; userContext: UserContext }) => Promise; - linkAccounts: (input: { - recipeUserId: RecipeUserId; - primaryUserId: string; - userContext: any; - }) => Promise< - | { - status: "OK"; - accountsAlreadyLinked: boolean; - user: User; - } - | { - status: "RECIPE_USER_ID_ALREADY_LINKED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR"; - primaryUserId: string; - user: User; - } - | { - status: "ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR"; - primaryUserId: string; - description: string; - } - | { - status: "INPUT_USER_IS_NOT_A_PRIMARY_USER"; - } - >; + getDefaultRequiredFactorsForUser(input: { + user: User; + tenantId: string; + userContext: UserContext; + }): Promise; }; export type APIOptions = { recipeImplementation: RecipeInterface; + recipeInstance: Recipe; config: TypeNormalisedInput; recipeId: string; isInServerlessEnv: boolean; @@ -147,7 +122,7 @@ export type APIInterface = { mfaInfoGET: (input: { options: APIOptions; session: SessionContainerInterface; - userContext: any; + userContext: UserContext; }) => Promise< | { status: "OK"; @@ -155,7 +130,19 @@ export type APIInterface = { isAlreadySetup: string[]; isAllowedToSetup: string[]; }; + email?: string; + phoneNumber?: string; } | GeneralErrorResponse >; }; + +export type GetFactorsSetupForUserFromOtherRecipesFunc = ( + user: User, + tenantConfig: TenantConfig, + userContext: UserContext +) => Promise; + +export type GetAllFactorsFromOtherRecipesFunc = ( + tenantConfig: TenantConfig +) => { factorIds: string[]; firstFactorIds: string[] }; diff --git a/lib/ts/recipe/multifactorauth/utils.ts b/lib/ts/recipe/multifactorauth/utils.ts index b9c449708..424a1c138 100644 --- a/lib/ts/recipe/multifactorauth/utils.ts +++ b/lib/ts/recipe/multifactorauth/utils.ts @@ -13,7 +13,7 @@ * under the License. */ -import { TypeInput, TypeNormalisedInput, RecipeInterface, APIInterface } from "./types"; +import { TypeInput, TypeNormalisedInput, RecipeInterface, APIInterface, MFAClaimValue } from "./types"; export function validateAndNormaliseUserInput(config?: TypeInput): TypeNormalisedInput { let override = { @@ -27,3 +27,11 @@ export function validateAndNormaliseUserInput(config?: TypeInput): TypeNormalise override, }; } + +export function checkFactorRequirement(req: string, completedFactors: MFAClaimValue["c"]) { + return { + id: req, + isValid: completedFactors[req] !== undefined, + message: "Not completed", + }; +} diff --git a/lib/ts/recipe/multitenancy/allowedDomainsClaim.ts b/lib/ts/recipe/multitenancy/allowedDomainsClaim.ts index 52239b26a..1425e3e2a 100644 --- a/lib/ts/recipe/multitenancy/allowedDomainsClaim.ts +++ b/lib/ts/recipe/multitenancy/allowedDomainsClaim.ts @@ -8,7 +8,7 @@ export class AllowedDomainsClaimClass extends PrimitiveArrayClaim { constructor() { super({ key: "st-t-dmns", - async fetchValue(_userId, _recipeUserId, tenantId, userContext) { + async fetchValue(_userId, _recipeUserId, tenantId, _currentPayload, userContext) { const recipe = Recipe.getInstanceOrThrowError(); if (recipe.getAllowedDomainsForTenantId === undefined) { diff --git a/lib/ts/recipe/multitenancy/api/loginMethods.ts b/lib/ts/recipe/multitenancy/api/loginMethods.ts index afc13542f..42fb41bc8 100644 --- a/lib/ts/recipe/multitenancy/api/loginMethods.ts +++ b/lib/ts/recipe/multitenancy/api/loginMethods.ts @@ -15,12 +15,13 @@ import { send200Response } from "../../../utils"; import { APIInterface, APIOptions } from "../"; +import { UserContext } from "../../../types"; export default async function loginMethodsAPI( apiImplementation: APIInterface, tenantId: string, options: APIOptions, - userContext: any + userContext: UserContext ): Promise { if (apiImplementation.loginMethodsGET === undefined) { return false; diff --git a/lib/ts/recipe/multitenancy/index.ts b/lib/ts/recipe/multitenancy/index.ts index ecb4c2632..315da14d5 100644 --- a/lib/ts/recipe/multitenancy/index.ts +++ b/lib/ts/recipe/multitenancy/index.ts @@ -14,10 +14,11 @@ */ import Recipe from "./recipe"; -import { RecipeInterface, APIOptions, APIInterface } from "./types"; +import { RecipeInterface, APIOptions, APIInterface, TenantConfig } from "./types"; import { ProviderConfig } from "../thirdparty/types"; import { AllowedDomainsClaim } from "./allowedDomainsClaim"; import RecipeUserId from "../../recipeUserId"; +import { getUserContext } from "../../utils"; export default class Wrapper { static init = Recipe.init; @@ -28,9 +29,12 @@ export default class Wrapper { emailPasswordEnabled?: boolean; passwordlessEnabled?: boolean; thirdPartyEnabled?: boolean; + totpEnabled?: boolean; + firstFactors?: string[]; + defaultRequiredFactorIds?: string[]; coreConfig?: { [key: string]: any }; }, - userContext?: any + userContext?: Record ): Promise<{ status: "OK"; createdNew: boolean; @@ -39,13 +43,13 @@ export default class Wrapper { return recipeInstance.recipeInterfaceImpl.createOrUpdateTenant({ tenantId, config, - userContext: userContext === undefined ? {} : userContext, + userContext: getUserContext(userContext), }); } static async deleteTenant( tenantId: string, - userContext?: any + userContext?: Record ): Promise<{ status: "OK"; didExist: boolean; @@ -53,59 +57,37 @@ export default class Wrapper { const recipeInstance = Recipe.getInstanceOrThrowError(); return recipeInstance.recipeInterfaceImpl.deleteTenant({ tenantId, - userContext: userContext === undefined ? {} : userContext, + userContext: getUserContext(userContext), }); } static async getTenant( tenantId: string, - userContext?: any + userContext?: Record ): Promise< - | { + | ({ status: "OK"; - emailPassword: { - enabled: boolean; - }; - passwordless: { - enabled: boolean; - }; - thirdParty: { - enabled: boolean; - providers: ProviderConfig[]; - }; - coreConfig: { [key: string]: any }; - } + } & TenantConfig) | undefined > { const recipeInstance = Recipe.getInstanceOrThrowError(); return recipeInstance.recipeInterfaceImpl.getTenant({ tenantId, - userContext: userContext === undefined ? {} : userContext, + userContext: getUserContext(userContext), }); } static async listAllTenants( - userContext?: any + userContext?: Record ): Promise<{ status: "OK"; - tenants: { + tenants: ({ tenantId: string; - emailPassword: { - enabled: boolean; - }; - passwordless: { - enabled: boolean; - }; - thirdParty: { - enabled: boolean; - providers: ProviderConfig[]; - }; - coreConfig: { [key: string]: any }; - }[]; + } & TenantConfig)[]; }> { const recipeInstance = Recipe.getInstanceOrThrowError(); return recipeInstance.recipeInterfaceImpl.listAllTenants({ - userContext: userContext === undefined ? {} : userContext, + userContext: getUserContext(userContext), }); } @@ -113,7 +95,7 @@ export default class Wrapper { tenantId: string, config: ProviderConfig, skipValidation?: boolean, - userContext?: any + userContext?: Record ): Promise<{ status: "OK"; createdNew: boolean; @@ -123,14 +105,14 @@ export default class Wrapper { tenantId, config, skipValidation, - userContext: userContext === undefined ? {} : userContext, + userContext: getUserContext(userContext), }); } static async deleteThirdPartyConfig( tenantId: string, thirdPartyId: string, - userContext?: any + userContext?: Record ): Promise<{ status: "OK"; didConfigExist: boolean; @@ -139,14 +121,14 @@ export default class Wrapper { return recipeInstance.recipeInterfaceImpl.deleteThirdPartyConfig({ tenantId, thirdPartyId, - userContext: userContext === undefined ? {} : userContext, + userContext: getUserContext(userContext), }); } static async associateUserToTenant( tenantId: string, recipeUserId: RecipeUserId, - userContext?: any + userContext?: Record ): Promise< | { status: "OK"; @@ -168,14 +150,14 @@ export default class Wrapper { return recipeInstance.recipeInterfaceImpl.associateUserToTenant({ tenantId, recipeUserId, - userContext: userContext === undefined ? {} : userContext, + userContext: getUserContext(userContext), }); } static async disassociateUserFromTenant( tenantId: string, recipeUserId: RecipeUserId, - userContext?: any + userContext?: Record ): Promise<{ status: "OK"; wasAssociated: boolean; @@ -184,7 +166,7 @@ export default class Wrapper { return recipeInstance.recipeInterfaceImpl.disassociateUserFromTenant({ tenantId, recipeUserId, - userContext: userContext === undefined ? {} : userContext, + userContext: getUserContext(userContext), }); } } diff --git a/lib/ts/recipe/multitenancy/recipe.ts b/lib/ts/recipe/multitenancy/recipe.ts index bbea4f4f2..f5e6f378c 100644 --- a/lib/ts/recipe/multitenancy/recipe.ts +++ b/lib/ts/recipe/multitenancy/recipe.ts @@ -20,7 +20,7 @@ import { PostSuperTokensInitCallbacks } from "../../postSuperTokensInitCallbacks import { Querier } from "../../querier"; import RecipeModule from "../../recipeModule"; import STError from "../../error"; -import { APIHandled, HTTPMethod, NormalisedAppinfo, RecipeListFunction } from "../../types"; +import { APIHandled, HTTPMethod, NormalisedAppinfo, RecipeListFunction, UserContext } from "../../types"; import RecipeImplementation from "./recipeImplementation"; import APIImplementation from "./api/implementation"; import SessionRecipe from "../session/recipe"; @@ -45,7 +45,7 @@ export default class Recipe extends RecipeModule { staticThirdPartyProviders: ProviderInput[] = []; - getAllowedDomainsForTenantId?: (tenantId: string, userContext: any) => Promise; + getAllowedDomainsForTenantId?: (tenantId: string, userContext: UserContext) => Promise; constructor(recipeId: string, appInfo: NormalisedAppinfo, isInServerlessEnv: boolean, config?: TypeInput) { super(recipeId, appInfo); @@ -125,7 +125,7 @@ export default class Recipe extends RecipeModule { res: BaseResponse, _: NormalisedURLPath, __: HTTPMethod, - userContext: any + userContext: UserContext ): Promise => { let options = { recipeImplementation: this.recipeInterfaceImpl, diff --git a/lib/ts/recipe/multitenancy/types.ts b/lib/ts/recipe/multitenancy/types.ts index 6fdab1a19..1b7284699 100644 --- a/lib/ts/recipe/multitenancy/types.ts +++ b/lib/ts/recipe/multitenancy/types.ts @@ -16,11 +16,11 @@ import { BaseRequest, BaseResponse } from "../../framework"; import OverrideableBuilder from "supertokens-js-override"; import { ProviderConfig, ProviderInput } from "../thirdparty/types"; -import { GeneralErrorResponse } from "../../types"; +import { GeneralErrorResponse, UserContext } from "../../types"; import RecipeUserId from "../../recipeUserId"; export type TypeInput = { - getAllowedDomainsForTenantId?: (tenantId: string, userContext: any) => Promise; + getAllowedDomainsForTenantId?: (tenantId: string, userContext: UserContext) => Promise; override?: { functions?: ( @@ -32,7 +32,7 @@ export type TypeInput = { }; export type TypeNormalisedInput = { - getAllowedDomainsForTenantId?: (tenantId: string, userContext: any) => Promise; + getAllowedDomainsForTenantId?: (tenantId: string, userContext: UserContext) => Promise; override: { functions: ( @@ -43,8 +43,27 @@ export type TypeNormalisedInput = { }; }; +export type TenantConfig = { + emailPassword: { + enabled: boolean; + }; + passwordless: { + enabled: boolean; + }; + thirdParty: { + enabled: boolean; + providers: ProviderConfig[]; + }; + totp: { + enabled: boolean; + }; + firstFactors?: string[]; + defaultRequiredFactorIds?: string[]; + coreConfig: { [key: string]: any }; +}; + export type RecipeInterface = { - getTenantId: (input: { tenantIdFromFrontend: string; userContext: any }) => Promise; + getTenantId: (input: { tenantIdFromFrontend: string; userContext: UserContext }) => Promise; // Tenant management createOrUpdateTenant: (input: { @@ -53,58 +72,37 @@ export type RecipeInterface = { emailPasswordEnabled?: boolean; passwordlessEnabled?: boolean; thirdPartyEnabled?: boolean; + totpEnabled?: boolean; + firstFactors?: string[]; + defaultRequiredFactorIds?: string[]; coreConfig?: { [key: string]: any }; }; - userContext: any; + userContext: UserContext; }) => Promise<{ status: "OK"; createdNew: boolean; }>; deleteTenant: (input: { tenantId: string; - userContext: any; + userContext: UserContext; }) => Promise<{ status: "OK"; didExist: boolean; }>; getTenant: (input: { tenantId: string; - userContext: any; + userContext: UserContext; }) => Promise< - | { + | ({ status: "OK"; - emailPassword: { - enabled: boolean; - }; - passwordless: { - enabled: boolean; - }; - thirdParty: { - enabled: boolean; - providers: ProviderConfig[]; - }; - coreConfig: { [key: string]: any }; - } + } & TenantConfig) | undefined >; listAllTenants: (input: { - userContext: any; + userContext: UserContext; }) => Promise<{ status: "OK"; - tenants: { - tenantId: string; - emailPassword: { - enabled: boolean; - }; - passwordless: { - enabled: boolean; - }; - thirdParty: { - enabled: boolean; - providers: ProviderConfig[]; - }; - coreConfig: { [key: string]: any }; - }[]; + tenants: (TenantConfig & { tenantId: string })[]; }>; // Third party provider management @@ -112,7 +110,7 @@ export type RecipeInterface = { tenantId: string; config: ProviderConfig; skipValidation?: boolean; - userContext: any; + userContext: UserContext; }) => Promise<{ status: "OK"; createdNew: boolean; @@ -120,7 +118,7 @@ export type RecipeInterface = { deleteThirdPartyConfig: (input: { tenantId: string; thirdPartyId: string; - userContext: any; + userContext: UserContext; }) => Promise<{ status: "OK"; didConfigExist: boolean; @@ -130,7 +128,7 @@ export type RecipeInterface = { associateUserToTenant: (input: { tenantId: string; recipeUserId: RecipeUserId; - userContext: any; + userContext: UserContext; }) => Promise< | { status: "OK"; @@ -151,7 +149,7 @@ export type RecipeInterface = { disassociateUserFromTenant: (input: { tenantId: string; recipeUserId: RecipeUserId; - userContext: any; + userContext: UserContext; }) => Promise<{ status: "OK"; wasAssociated: boolean; @@ -173,7 +171,7 @@ export type APIInterface = { tenantId: string; clientType?: string; options: APIOptions; - userContext: any; + userContext: UserContext; }) => Promise< | { status: "OK"; diff --git a/lib/ts/recipe/openid/api/getOpenIdDiscoveryConfiguration.ts b/lib/ts/recipe/openid/api/getOpenIdDiscoveryConfiguration.ts index bc82eb475..90c291574 100644 --- a/lib/ts/recipe/openid/api/getOpenIdDiscoveryConfiguration.ts +++ b/lib/ts/recipe/openid/api/getOpenIdDiscoveryConfiguration.ts @@ -12,13 +12,14 @@ * License for the specific language governing permissions and limitations * under the License. */ +import { UserContext } from "../../../types"; import { send200Response } from "../../../utils"; import { APIInterface, APIOptions } from "../types"; export default async function getOpenIdDiscoveryConfiguration( apiImplementation: APIInterface, options: APIOptions, - userContext: any + userContext: UserContext ): Promise { if (apiImplementation.getOpenIdDiscoveryConfigurationGET === undefined) { return false; diff --git a/lib/ts/recipe/openid/api/implementation.ts b/lib/ts/recipe/openid/api/implementation.ts index 19bf1a4b6..ee1f83e21 100644 --- a/lib/ts/recipe/openid/api/implementation.ts +++ b/lib/ts/recipe/openid/api/implementation.ts @@ -13,7 +13,7 @@ * under the License. */ import { APIInterface, APIOptions } from "../types"; -import { GeneralErrorResponse } from "../../../types"; +import { GeneralErrorResponse, UserContext } from "../../../types"; export default function getAPIImplementation(): APIInterface { return { @@ -22,7 +22,7 @@ export default function getAPIImplementation(): APIInterface { userContext, }: { options: APIOptions; - userContext: any; + userContext: UserContext; }): Promise<{ status: "OK"; issuer: string; jwks_uri: string } | GeneralErrorResponse> { return await options.recipeImplementation.getOpenIdDiscoveryConfiguration({ userContext }); }, diff --git a/lib/ts/recipe/openid/index.ts b/lib/ts/recipe/openid/index.ts index 23d835b39..cf69bc4b6 100644 --- a/lib/ts/recipe/openid/index.ts +++ b/lib/ts/recipe/openid/index.ts @@ -1,26 +1,32 @@ +import { getUserContext } from "../../utils"; import OpenIdRecipe from "./recipe"; export default class OpenIdRecipeWrapper { static init = OpenIdRecipe.init; - static getOpenIdDiscoveryConfiguration(userContext?: any) { + static getOpenIdDiscoveryConfiguration(userContext?: Record) { return OpenIdRecipe.getInstanceOrThrowError().recipeImplementation.getOpenIdDiscoveryConfiguration({ - userContext: userContext === undefined ? {} : userContext, + userContext: getUserContext(userContext), }); } - static createJWT(payload?: any, validitySeconds?: number, useStaticSigningKey?: boolean, userContext?: any) { + static createJWT( + payload?: any, + validitySeconds?: number, + useStaticSigningKey?: boolean, + userContext?: Record + ) { return OpenIdRecipe.getInstanceOrThrowError().jwtRecipe.recipeInterfaceImpl.createJWT({ payload, validitySeconds, useStaticSigningKey, - userContext: userContext === undefined ? {} : userContext, + userContext: getUserContext(userContext), }); } - static getJWKS(userContext?: any) { + static getJWKS(userContext?: Record) { return OpenIdRecipe.getInstanceOrThrowError().jwtRecipe.recipeInterfaceImpl.getJWKS({ - userContext: userContext === undefined ? {} : userContext, + userContext: getUserContext(userContext), }); } } diff --git a/lib/ts/recipe/openid/recipe.ts b/lib/ts/recipe/openid/recipe.ts index 27362aea7..f2d070a84 100644 --- a/lib/ts/recipe/openid/recipe.ts +++ b/lib/ts/recipe/openid/recipe.ts @@ -16,7 +16,7 @@ import STError from "../../error"; import type { BaseRequest, BaseResponse } from "../../framework"; import normalisedURLPath from "../../normalisedURLPath"; import RecipeModule from "../../recipeModule"; -import { APIHandled, HTTPMethod, NormalisedAppinfo, RecipeListFunction } from "../../types"; +import { APIHandled, HTTPMethod, NormalisedAppinfo, RecipeListFunction, UserContext } from "../../types"; import { APIInterface, APIOptions, RecipeInterface, TypeInput, TypeNormalisedInput } from "./types"; import { validateAndNormaliseUserInput } from "./utils"; import JWTRecipe from "../jwt/recipe"; @@ -96,7 +96,7 @@ export default class OpenIdRecipe extends RecipeModule { response: BaseResponse, path: normalisedURLPath, method: HTTPMethod, - userContext: any + userContext: UserContext ): Promise => { let apiOptions: APIOptions = { recipeImplementation: this.recipeImplementation, @@ -112,11 +112,16 @@ export default class OpenIdRecipe extends RecipeModule { return this.jwtRecipe.handleAPIRequest(id, tenantId, req, response, path, method, userContext); } }; - handleError = async (error: STError, request: BaseRequest, response: BaseResponse): Promise => { + handleError = async ( + error: STError, + request: BaseRequest, + response: BaseResponse, + userContext: UserContext + ): Promise => { if (error.fromRecipe === OpenIdRecipe.RECIPE_ID) { throw error; } else { - return await this.jwtRecipe.handleError(error, request, response); + return await this.jwtRecipe.handleError(error, request, response, userContext); } }; getAllCORSHeaders = (): string[] => { diff --git a/lib/ts/recipe/openid/recipeImplementation.ts b/lib/ts/recipe/openid/recipeImplementation.ts index c79fde8be..f161e8d23 100644 --- a/lib/ts/recipe/openid/recipeImplementation.ts +++ b/lib/ts/recipe/openid/recipeImplementation.ts @@ -16,6 +16,7 @@ import { RecipeInterface, TypeNormalisedInput } from "./types"; import { RecipeInterface as JWTRecipeInterface, JsonWebKey } from "../jwt/types"; import NormalisedURLPath from "../../normalisedURLPath"; import { GET_JWKS_API } from "../jwt/constants"; +import { UserContext } from "../../types"; export default function getRecipeInterface( config: TypeNormalisedInput, @@ -46,7 +47,7 @@ export default function getRecipeInterface( payload?: any; validitySeconds?: number; useStaticSigningKey?: boolean; - userContext: any; + userContext: UserContext; }): Promise< | { status: "OK"; diff --git a/lib/ts/recipe/openid/types.ts b/lib/ts/recipe/openid/types.ts index e5e4c5124..c303cb0ca 100644 --- a/lib/ts/recipe/openid/types.ts +++ b/lib/ts/recipe/openid/types.ts @@ -17,7 +17,7 @@ import type { BaseRequest, BaseResponse } from "../../framework"; import NormalisedURLDomain from "../../normalisedURLDomain"; import NormalisedURLPath from "../../normalisedURLPath"; import { RecipeInterface as JWTRecipeInterface, APIInterface as JWTAPIInterface, JsonWebKey } from "../jwt/types"; -import { GeneralErrorResponse } from "../../types"; +import { GeneralErrorResponse, UserContext } from "../../types"; export type TypeInput = { issuer?: string; @@ -77,7 +77,7 @@ export type APIInterface = { | undefined | ((input: { options: APIOptions; - userContext: any; + userContext: UserContext; }) => Promise< | { status: "OK"; @@ -90,7 +90,7 @@ export type APIInterface = { export type RecipeInterface = { getOpenIdDiscoveryConfiguration(input: { - userContext: any; + userContext: UserContext; }): Promise<{ status: "OK"; issuer: string; @@ -100,7 +100,7 @@ export type RecipeInterface = { payload?: any; validitySeconds?: number; useStaticSigningKey?: boolean; - userContext: any; + userContext: UserContext; }): Promise< | { status: "OK"; @@ -112,7 +112,7 @@ export type RecipeInterface = { >; getJWKS(input: { - userContext: any; + userContext: UserContext; }): Promise<{ keys: JsonWebKey[]; }>; diff --git a/lib/ts/recipe/passwordless/api/consumeCode.ts b/lib/ts/recipe/passwordless/api/consumeCode.ts index 944d665f8..55f182204 100644 --- a/lib/ts/recipe/passwordless/api/consumeCode.ts +++ b/lib/ts/recipe/passwordless/api/consumeCode.ts @@ -16,12 +16,13 @@ import { getBackwardsCompatibleUserInfo, send200Response } from "../../../utils"; import STError from "../error"; import { APIInterface, APIOptions } from ".."; +import { UserContext } from "../../../types"; export default async function consumeCode( apiImplementation: APIInterface, tenantId: string, options: APIOptions, - userContext: any + userContext: UserContext ): Promise { if (apiImplementation.consumeCodePOST === undefined) { return false; diff --git a/lib/ts/recipe/passwordless/api/createCode.ts b/lib/ts/recipe/passwordless/api/createCode.ts index bd94a27b9..002cdc377 100644 --- a/lib/ts/recipe/passwordless/api/createCode.ts +++ b/lib/ts/recipe/passwordless/api/createCode.ts @@ -17,12 +17,13 @@ import { send200Response } from "../../../utils"; import STError from "../error"; import { APIInterface, APIOptions } from ".."; import parsePhoneNumber from "libphonenumber-js/max"; +import { UserContext } from "../../../types"; export default async function createCode( apiImplementation: APIInterface, tenantId: string, options: APIOptions, - userContext: any + userContext: UserContext ): Promise { if (apiImplementation.createCodePOST === undefined) { return false; diff --git a/lib/ts/recipe/passwordless/api/emailExists.ts b/lib/ts/recipe/passwordless/api/emailExists.ts index 43b6523a5..ae0756774 100644 --- a/lib/ts/recipe/passwordless/api/emailExists.ts +++ b/lib/ts/recipe/passwordless/api/emailExists.ts @@ -16,12 +16,13 @@ import { send200Response } from "../../../utils"; import STError from "../error"; import { APIInterface, APIOptions } from "../"; +import { UserContext } from "../../../types"; export default async function emailExists( apiImplementation: APIInterface, tenantId: string, options: APIOptions, - userContext: any + userContext: UserContext ): Promise { if (apiImplementation.emailExistsGET === undefined) { return false; diff --git a/lib/ts/recipe/passwordless/api/implementation.ts b/lib/ts/recipe/passwordless/api/implementation.ts index 1b34bdade..79ca2e86a 100644 --- a/lib/ts/recipe/passwordless/api/implementation.ts +++ b/lib/ts/recipe/passwordless/api/implementation.ts @@ -1,9 +1,13 @@ import { APIInterface } from "../"; import { logDebugMessage } from "../../../logger"; import AccountLinking from "../../accountlinking/recipe"; +import MultiFactorAuthRecipe from "../../multifactorauth/recipe"; import Session from "../../session"; -import { listUsersByAccountInfo } from "../../.."; +import { User, getUser, listUsersByAccountInfo } from "../../.."; import { RecipeLevelUser } from "../../accountlinking/types"; +import { SessionContainerInterface } from "../../session/types"; +import { isFactorSetupForUser } from "../utils"; +import SessionError from "../../session/error"; export default function getAPIImplementation(): APIInterface { return { @@ -63,6 +67,59 @@ export default function getAPIImplementation(): APIInterface { "You have found a bug. Please report it on https://github.com/supertokens/supertokens-node/issues" ); } + const userLoggingIn = existingUsers[0]; + + const mfaInstance = MultiFactorAuthRecipe.getInstance(); + + let session: SessionContainerInterface | undefined = await Session.getSession( + input.options.req, + input.options.res, + { sessionRequired: false, overrideGlobalClaimValidators: () => [] } + ); + let sessionUser: User | undefined; + if (session !== undefined) { + if (userLoggingIn && userLoggingIn.id === session.getUserId()) { + sessionUser = userLoggingIn; // optimization + } else { + const user = await getUser(session.getUserId(), input.userContext); + if (user === undefined) { + throw new SessionError({ + type: SessionError.UNAUTHORISED, + message: "Session user not found", + }); + } + sessionUser = user; + } + } + + const factorId = `${"userInputCode" in input ? "otp" : "link"}-${deviceInfo.email ? "email" : "phone"}`; + let isAlreadySetup = undefined; + + if (mfaInstance) { + isAlreadySetup = !sessionUser + ? false + : isFactorSetupForUser(sessionUser, factorId) && + (deviceInfo.email + ? sessionUser.emails.includes(deviceInfo.email) + : sessionUser.phoneNumbers.includes(deviceInfo.phoneNumber!)); + // We want to consider a factor as already setup only if email/phoneNumber of the userLoggingIn matches with the sessionUser emails/phoneNumbers + // because if it's a different email/phone number, it means we might be setting up that factor + const validateMfaRes = await mfaInstance.validateForMultifactorAuthBeforeFactorCompletion({ + req: input.options.req, + res: input.options.res, + tenantId: input.tenantId, + factorIdInProgress: factorId, + session, + userLoggingIn, + isAlreadySetup, + signUpInfo: deviceInfo.email ? { email: deviceInfo.email, isVerifiedFactor: true } : undefined, + userContext: input.userContext, + }); + + if (validateMfaRes.status !== "OK") { + return validateMfaRes; + } + } let response = await input.options.recipeImplementation.consumeCode( "deviceId" in input @@ -71,12 +128,16 @@ export default function getAPIImplementation(): APIInterface { deviceId: input.deviceId, userInputCode: input.userInputCode, tenantId: input.tenantId, + // we do not want to attempt accountlinking when there is an active session and MFA is turned on + shouldAttemptAccountLinkingIfAllowed: session === undefined || mfaInstance === undefined, userContext: input.userContext, } : { preAuthSessionId: input.preAuthSessionId, linkCode: input.linkCode, tenantId: input.tenantId, + // we do not want to attempt accountlinking when there is an active session and MFA is turned on + shouldAttemptAccountLinkingIfAllowed: session === undefined || mfaInstance === undefined, userContext: input.userContext, } ); @@ -119,27 +180,58 @@ export default function getAPIImplementation(): APIInterface { // we do account linking only during sign in here cause during sign up, // the recipe function above does account linking for us. - response.user = await AccountLinking.getInstance().createPrimaryUserIdOrLinkAccounts({ - tenantId: input.tenantId, + // we do not want to attempt accountlinking when there is an active session and MFA is turned on + if (session === undefined || mfaInstance === undefined) { + response.user = await AccountLinking.getInstance().createPrimaryUserIdOrLinkAccounts({ + tenantId: input.tenantId, + user: response.user, + userContext: input.userContext, + }); + } + } + + if (mfaInstance === undefined) { + // No MFA stuff here, so we just create and return the session + let session = await Session.createNewOrKeepExistingSession( + input.options.req, + input.options.res, + input.tenantId, + response.recipeUserId, + {}, + {}, + input.userContext + ); + + return { + status: "OK", + createdNewRecipeUser: response.createdNewRecipeUser, user: response.user, - userContext: input.userContext, - }); + session, + }; } - const session = await Session.createNewSession( - input.options.req, - input.options.res, - input.tenantId, - loginMethod.recipeUserId, - {}, - {}, - input.userContext - ); + const sessionRes = await mfaInstance.createOrUpdateSessionForMultifactorAuthAfterFactorCompletion({ + req: input.options.req, + res: input.options.res, + tenantId: input.tenantId, + factorIdInProgress: factorId, + justCompletedFactorUserInfo: { + user: response.user, + createdNewUser: response.createdNewRecipeUser, + recipeUserId: response.recipeUserId, + }, + isAlreadySetup, + userContext: input.userContext, + }); + if (sessionRes.status !== "OK") { + return sessionRes; + } + session = sessionRes.session; return { status: "OK", createdNewRecipeUser: response.createdNewRecipeUser, - user: response.user, + user: (await getUser(response.user.id, input.userContext))!, // fetching user again cause the user might have been updated while setting up mfa session, }; }, diff --git a/lib/ts/recipe/passwordless/api/phoneNumberExists.ts b/lib/ts/recipe/passwordless/api/phoneNumberExists.ts index ab45fb323..9708681a5 100644 --- a/lib/ts/recipe/passwordless/api/phoneNumberExists.ts +++ b/lib/ts/recipe/passwordless/api/phoneNumberExists.ts @@ -16,12 +16,13 @@ import { send200Response } from "../../../utils"; import STError from "../error"; import { APIInterface, APIOptions } from ".."; +import { UserContext } from "../../../types"; export default async function phoneNumberExists( apiImplementation: APIInterface, tenantId: string, options: APIOptions, - userContext: any + userContext: UserContext ): Promise { if (apiImplementation.phoneNumberExistsGET === undefined) { return false; diff --git a/lib/ts/recipe/passwordless/api/resendCode.ts b/lib/ts/recipe/passwordless/api/resendCode.ts index 929624e29..57dcb020f 100644 --- a/lib/ts/recipe/passwordless/api/resendCode.ts +++ b/lib/ts/recipe/passwordless/api/resendCode.ts @@ -16,12 +16,13 @@ import { send200Response } from "../../../utils"; import STError from "../error"; import { APIInterface, APIOptions } from ".."; +import { UserContext } from "../../../types"; export default async function resendCode( apiImplementation: APIInterface, tenantId: string, options: APIOptions, - userContext: any + userContext: UserContext ): Promise { if (apiImplementation.resendCodePOST === undefined) { return false; diff --git a/lib/ts/recipe/passwordless/emaildelivery/services/backwardCompatibility/index.ts b/lib/ts/recipe/passwordless/emaildelivery/services/backwardCompatibility/index.ts index 71df29022..fd5fde3d8 100644 --- a/lib/ts/recipe/passwordless/emaildelivery/services/backwardCompatibility/index.ts +++ b/lib/ts/recipe/passwordless/emaildelivery/services/backwardCompatibility/index.ts @@ -14,7 +14,7 @@ */ import { TypePasswordlessEmailDeliveryInput } from "../../../types"; import { EmailDeliveryInterface } from "../../../../../ingredients/emaildelivery/types"; -import { NormalisedAppinfo } from "../../../../../types"; +import { NormalisedAppinfo, UserContext } from "../../../../../types"; import { postWithFetch } from "../../../../../utils"; async function createAndSendEmailUsingSupertokensService(input: { @@ -72,7 +72,7 @@ export default class BackwardCompatibilityService this.appInfo = appInfo; } - sendEmail = async (input: TypePasswordlessEmailDeliveryInput & { userContext: any }) => { + sendEmail = async (input: TypePasswordlessEmailDeliveryInput & { userContext: UserContext }) => { await createAndSendEmailUsingSupertokensService({ appInfo: this.appInfo, email: input.email, diff --git a/lib/ts/recipe/passwordless/emaildelivery/services/smtp/index.ts b/lib/ts/recipe/passwordless/emaildelivery/services/smtp/index.ts index 273b535f7..8f2e67549 100644 --- a/lib/ts/recipe/passwordless/emaildelivery/services/smtp/index.ts +++ b/lib/ts/recipe/passwordless/emaildelivery/services/smtp/index.ts @@ -18,6 +18,7 @@ import { createTransport } from "nodemailer"; import OverrideableBuilder from "supertokens-js-override"; import { TypePasswordlessEmailDeliveryInput } from "../../../types"; import { getServiceImplementation } from "./serviceImplementation"; +import { UserContext } from "../../../../../types"; export default class SMTPService implements EmailDeliveryInterface { serviceImpl: ServiceInterface; @@ -39,7 +40,7 @@ export default class SMTPService implements EmailDeliveryInterface { + sendEmail = async (input: TypePasswordlessEmailDeliveryInput & { userContext: UserContext }) => { let content = await this.serviceImpl.getContent(input); await this.serviceImpl.sendRawEmail({ ...content, diff --git a/lib/ts/recipe/passwordless/emaildelivery/services/smtp/serviceImplementation.ts b/lib/ts/recipe/passwordless/emaildelivery/services/smtp/serviceImplementation.ts index 067838bba..91f2ce1c8 100644 --- a/lib/ts/recipe/passwordless/emaildelivery/services/smtp/serviceImplementation.ts +++ b/lib/ts/recipe/passwordless/emaildelivery/services/smtp/serviceImplementation.ts @@ -21,6 +21,7 @@ import { GetContentResult, } from "../../../../../ingredients/emaildelivery/services/smtp"; import getPasswordlessLoginEmailContent from "./passwordlessLogin"; +import { UserContext } from "../../../../../types"; export function getServiceImplementation( transporter: Transporter, @@ -48,7 +49,7 @@ export function getServiceImplementation( } }, getContent: async function ( - input: TypePasswordlessEmailDeliveryInput & { userContext: any } + input: TypePasswordlessEmailDeliveryInput & { userContext: UserContext } ): Promise { return getPasswordlessLoginEmailContent(input); }, diff --git a/lib/ts/recipe/passwordless/index.ts b/lib/ts/recipe/passwordless/index.ts index 36110c883..3c66cafb2 100644 --- a/lib/ts/recipe/passwordless/index.ts +++ b/lib/ts/recipe/passwordless/index.ts @@ -24,6 +24,7 @@ import { } from "./types"; import RecipeUserId from "../../recipeUserId"; import { getRequestFromUserContext } from "../.."; +import { getUserContext } from "../../utils"; export default class Wrapper { static init = Recipe.init; @@ -38,11 +39,11 @@ export default class Wrapper { | { phoneNumber: string; } - ) & { tenantId: string; userInputCode?: string; userContext?: any } + ) & { tenantId: string; userInputCode?: string; userContext?: Record } ) { return Recipe.getInstanceOrThrowError().recipeInterfaceImpl.createCode({ ...input, - userContext: input.userContext ?? {}, + userContext: getUserContext(input.userContext), }); } @@ -50,11 +51,11 @@ export default class Wrapper { deviceId: string; userInputCode?: string; tenantId: string; - userContext?: any; + userContext?: Record; }) { return Recipe.getInstanceOrThrowError().recipeInterfaceImpl.createNewCodeForDevice({ ...input, - userContext: input.userContext ?? {}, + userContext: getUserContext(input.userContext), }); } @@ -65,18 +66,20 @@ export default class Wrapper { userInputCode: string; deviceId: string; tenantId: string; - userContext?: any; + shouldAttemptAccountLinkingIfAllowed?: boolean; + userContext?: Record; } | { preAuthSessionId: string; linkCode: string; tenantId: string; - userContext?: any; + shouldAttemptAccountLinkingIfAllowed?: boolean; + userContext?: Record; } ) { return Recipe.getInstanceOrThrowError().recipeInterfaceImpl.consumeCode({ ...input, - userContext: input.userContext ?? {}, + userContext: getUserContext(input.userContext), }); } @@ -84,9 +87,12 @@ export default class Wrapper { recipeUserId: RecipeUserId; email?: string | null; phoneNumber?: string | null; - userContext?: any; + userContext?: Record; }) { - return Recipe.getInstanceOrThrowError().recipeInterfaceImpl.updateUser({ userContext: {}, ...input }); + return Recipe.getInstanceOrThrowError().recipeInterfaceImpl.updateUser({ + ...input, + userContext: getUserContext(input.userContext), + }); } static revokeAllCodes( @@ -94,52 +100,56 @@ export default class Wrapper { | { email: string; tenantId: string; - userContext?: any; + userContext?: Record; } | { phoneNumber: string; tenantId: string; - userContext?: any; + userContext?: Record; } ) { return Recipe.getInstanceOrThrowError().recipeInterfaceImpl.revokeAllCodes({ ...input, - userContext: input.userContext ?? {}, + userContext: getUserContext(input.userContext), }); } - static revokeCode(input: { codeId: string; tenantId: string; userContext?: any }) { + static revokeCode(input: { codeId: string; tenantId: string; userContext?: Record }) { return Recipe.getInstanceOrThrowError().recipeInterfaceImpl.revokeCode({ ...input, - userContext: input.userContext ?? {}, + userContext: getUserContext(input.userContext), }); } - static listCodesByEmail(input: { email: string; tenantId: string; userContext?: any }) { + static listCodesByEmail(input: { email: string; tenantId: string; userContext?: Record }) { return Recipe.getInstanceOrThrowError().recipeInterfaceImpl.listCodesByEmail({ ...input, - userContext: input.userContext ?? {}, + userContext: getUserContext(input.userContext), }); } - static listCodesByPhoneNumber(input: { phoneNumber: string; tenantId: string; userContext?: any }) { + static listCodesByPhoneNumber(input: { phoneNumber: string; tenantId: string; userContext?: Record }) { return Recipe.getInstanceOrThrowError().recipeInterfaceImpl.listCodesByPhoneNumber({ ...input, - userContext: input.userContext ?? {}, + userContext: getUserContext(input.userContext), }); } - static listCodesByDeviceId(input: { deviceId: string; tenantId: string; userContext?: any }) { + static listCodesByDeviceId(input: { deviceId: string; tenantId: string; userContext?: Record }) { return Recipe.getInstanceOrThrowError().recipeInterfaceImpl.listCodesByDeviceId({ ...input, - userContext: input.userContext ?? {}, + userContext: getUserContext(input.userContext), }); } - static listCodesByPreAuthSessionId(input: { preAuthSessionId: string; tenantId: string; userContext?: any }) { + static listCodesByPreAuthSessionId(input: { + preAuthSessionId: string; + tenantId: string; + userContext?: Record; + }) { return Recipe.getInstanceOrThrowError().recipeInterfaceImpl.listCodesByPreAuthSessionId({ ...input, - userContext: input.userContext ?? {}, + userContext: getUserContext(input.userContext), }); } @@ -148,18 +158,19 @@ export default class Wrapper { | { email: string; tenantId: string; - userContext?: any; + userContext?: Record; } | { phoneNumber: string; tenantId: string; - userContext?: any; + userContext?: Record; } ) { + const ctx = getUserContext(input.userContext); return Recipe.getInstanceOrThrowError().createMagicLink({ ...input, - request: getRequestFromUserContext(input.userContext), - userContext: input.userContext ?? {}, + request: getRequestFromUserContext(ctx), + userContext: ctx, }); } @@ -168,31 +179,33 @@ export default class Wrapper { | { email: string; tenantId: string; - userContext?: any; + shouldAttemptAccountLinkingIfAllowed?: boolean; + userContext?: Record; } | { phoneNumber: string; tenantId: string; - userContext?: any; + shouldAttemptAccountLinkingIfAllowed?: boolean; + userContext?: Record; } ) { return Recipe.getInstanceOrThrowError().signInUp({ ...input, - userContext: input.userContext ?? {}, + userContext: getUserContext(input.userContext), }); } - static async sendEmail(input: TypePasswordlessEmailDeliveryInput & { userContext?: any }) { + static async sendEmail(input: TypePasswordlessEmailDeliveryInput & { userContext?: Record }) { return await Recipe.getInstanceOrThrowError().emailDelivery.ingredientInterfaceImpl.sendEmail({ ...input, - userContext: input.userContext ?? {}, + userContext: getUserContext(input.userContext), }); } - static async sendSms(input: TypePasswordlessSmsDeliveryInput & { userContext?: any }) { + static async sendSms(input: TypePasswordlessSmsDeliveryInput & { userContext?: Record }) { return await Recipe.getInstanceOrThrowError().smsDelivery.ingredientInterfaceImpl.sendSms({ ...input, - userContext: input.userContext ?? {}, + userContext: getUserContext(input.userContext), }); } } diff --git a/lib/ts/recipe/passwordless/recipe.ts b/lib/ts/recipe/passwordless/recipe.ts index 374f0ad99..bb2a811ac 100644 --- a/lib/ts/recipe/passwordless/recipe.ts +++ b/lib/ts/recipe/passwordless/recipe.ts @@ -15,7 +15,7 @@ import RecipeModule from "../../recipeModule"; import { TypeInput, TypeNormalisedInput, RecipeInterface, APIInterface } from "./types"; -import { NormalisedAppinfo, APIHandled, RecipeListFunction, HTTPMethod } from "../../types"; +import { NormalisedAppinfo, APIHandled, RecipeListFunction, HTTPMethod, UserContext } from "../../types"; import STError from "./error"; import { validateAndNormaliseUserInput } from "./utils"; import NormalisedURLPath from "../../normalisedURLPath"; @@ -39,6 +39,11 @@ import { import EmailDeliveryIngredient from "../../ingredients/emaildelivery"; import { TypePasswordlessEmailDeliveryInput, TypePasswordlessSmsDeliveryInput } from "./types"; import SmsDeliveryIngredient from "../../ingredients/smsdelivery"; +import { PostSuperTokensInitCallbacks } from "../../postSuperTokensInitCallbacks"; +import MultiFactorAuthRecipe from "../multifactorauth/recipe"; +import { User } from "../../user"; +import { isFactorSetupForUser } from "./utils"; +import { TenantConfig } from "../multitenancy/types"; export default class Recipe extends RecipeModule { private static instance: Recipe | undefined = undefined; @@ -108,6 +113,62 @@ export default class Recipe extends RecipeModule { emailDelivery: undefined, smsDelivery: undefined, }); + + let otpOrLink: string[] = []; + let emailOrPhone: string[] = []; + + if (Recipe.instance.config.flowType === "MAGIC_LINK") { + otpOrLink.push("link"); + } else if (Recipe.instance.config.flowType === "USER_INPUT_CODE") { + otpOrLink.push("otp"); + } else { + otpOrLink.push("otp"); + otpOrLink.push("link"); + } + + if (Recipe.instance.config.contactMethod === "EMAIL") { + emailOrPhone.push("email"); + } else if (Recipe.instance.config.contactMethod === "PHONE") { + emailOrPhone.push("phone"); + } else { + emailOrPhone.push("email"); + emailOrPhone.push("phone"); + } + + const allFactors: string[] = []; + for (const ol of otpOrLink) { + for (const ep of emailOrPhone) { + allFactors.push(`${ol}-${ep}`); + } + } + + PostSuperTokensInitCallbacks.addPostInitCallback(() => { + const mfaInstance = MultiFactorAuthRecipe.getInstance(); + + if (mfaInstance !== undefined) { + mfaInstance.addGetAllFactorsFromOtherRecipesFunc((tenantConfig) => { + if (tenantConfig.passwordless.enabled === false) { + return { + factorIds: [], + firstFactorIds: [], + }; + } + return { + factorIds: allFactors, + firstFactorIds: allFactors, + }; + }); + mfaInstance.addGetFactorsSetupForUserFromOtherRecipes( + async (user: User, tenantConfig: TenantConfig) => { + if (tenantConfig.passwordless.enabled === false) { + return []; + } + return allFactors.filter((id) => isFactorSetupForUser(user, id)); + } + ); + } + }); + return Recipe.instance; } else { throw new Error("Passwordless recipe has already been initialised. Please check your code for bugs."); @@ -166,7 +227,7 @@ export default class Recipe extends RecipeModule { res: BaseResponse, _: NormalisedURLPath, __: HTTPMethod, - userContext: any + userContext: UserContext ): Promise => { const options = { config: this.config, @@ -212,13 +273,13 @@ export default class Recipe extends RecipeModule { email: string; tenantId: string; request: BaseRequest | undefined; - userContext?: any; + userContext: UserContext; } | { phoneNumber: string; tenantId: string; request: BaseRequest | undefined; - userContext?: any; + userContext: UserContext; } ): Promise => { let userInputCode = @@ -270,12 +331,14 @@ export default class Recipe extends RecipeModule { | { email: string; tenantId: string; - userContext?: any; + shouldAttemptAccountLinkingIfAllowed?: boolean; + userContext: UserContext; } | { phoneNumber: string; tenantId: string; - userContext?: any; + shouldAttemptAccountLinkingIfAllowed?: boolean; + userContext: UserContext; } ) => { let codeInfo = await this.recipeInterfaceImpl.createCode( @@ -298,6 +361,7 @@ export default class Recipe extends RecipeModule { preAuthSessionId: codeInfo.preAuthSessionId, linkCode: codeInfo.linkCode, tenantId: input.tenantId, + shouldAttemptAccountLinkingIfAllowed: input.shouldAttemptAccountLinkingIfAllowed, userContext: input.userContext, } : { @@ -305,6 +369,7 @@ export default class Recipe extends RecipeModule { deviceId: codeInfo.deviceId, userInputCode: codeInfo.userInputCode, tenantId: input.tenantId, + shouldAttemptAccountLinkingIfAllowed: input.shouldAttemptAccountLinkingIfAllowed, userContext: input.userContext, } ); @@ -315,7 +380,6 @@ export default class Recipe extends RecipeModule { createdNewRecipeUser: consumeCodeResponse.createdNewRecipeUser, recipeUserId: consumeCodeResponse.recipeUserId, user: consumeCodeResponse.user, - isValidFirstFactorForTenant: consumeCodeResponse.isValidFirstFactorForTenant, }; } else { throw new Error("Failed to create user. Please retry"); diff --git a/lib/ts/recipe/passwordless/recipeImplementation.ts b/lib/ts/recipe/passwordless/recipeImplementation.ts index c5bd3940a..fa1cc53da 100644 --- a/lib/ts/recipe/passwordless/recipeImplementation.ts +++ b/lib/ts/recipe/passwordless/recipeImplementation.ts @@ -58,15 +58,18 @@ export default function getRecipeInterface(querier: Querier): RecipeInterface { createdNewRecipeUser: response.createdNewUser, user: response.user, recipeUserId: response.recipeUserId, - isValidFirstFactorForTenant: response.isValidFirstFactorForTenant, }; } - let updatedUser = await AccountLinking.getInstance().createPrimaryUserIdOrLinkAccounts({ - tenantId: input.tenantId, - user: response.user, - userContext: input.userContext, - }); + let updatedUser = response.user; + + if (input.shouldAttemptAccountLinkingIfAllowed ?? true) { + updatedUser = await AccountLinking.getInstance().createPrimaryUserIdOrLinkAccounts({ + tenantId: input.tenantId, + user: response.user, + userContext: input.userContext, + }); + } if (updatedUser === undefined) { throw new Error("Should never come here."); @@ -76,7 +79,6 @@ export default function getRecipeInterface(querier: Querier): RecipeInterface { createdNewRecipeUser: response.createdNewUser, user: updatedUser, recipeUserId: response.recipeUserId, - isValidFirstFactorForTenant: response.isValidFirstFactorForTenant, }; }, createCode: async function (input) { diff --git a/lib/ts/recipe/passwordless/smsdelivery/services/backwardCompatibility/index.ts b/lib/ts/recipe/passwordless/smsdelivery/services/backwardCompatibility/index.ts index 06c140633..d592f08e9 100644 --- a/lib/ts/recipe/passwordless/smsdelivery/services/backwardCompatibility/index.ts +++ b/lib/ts/recipe/passwordless/smsdelivery/services/backwardCompatibility/index.ts @@ -17,6 +17,7 @@ import { SmsDeliveryInterface } from "../../../../../ingredients/smsdelivery/typ import { SUPERTOKENS_SMS_SERVICE_URL } from "../../../../../ingredients/smsdelivery/services/supertokens"; import Supertokens from "../../../../../supertokens"; import { postWithFetch } from "../../../../../utils"; +import { UserContext } from "../../../../../types"; async function createAndSendSmsUsingSupertokensService(input: { // Where the message should be delivered. @@ -102,7 +103,7 @@ async function createAndSendSmsUsingSupertokensService(input: { export default class BackwardCompatibilityService implements SmsDeliveryInterface { constructor() {} - sendSms = async (input: TypePasswordlessSmsDeliveryInput & { userContext: any }) => { + sendSms = async (input: TypePasswordlessSmsDeliveryInput & { userContext: UserContext }) => { await createAndSendSmsUsingSupertokensService({ phoneNumber: input.phoneNumber, userInputCode: input.userInputCode, diff --git a/lib/ts/recipe/passwordless/smsdelivery/services/twilio/index.ts b/lib/ts/recipe/passwordless/smsdelivery/services/twilio/index.ts index 6885830de..db551aad8 100644 --- a/lib/ts/recipe/passwordless/smsdelivery/services/twilio/index.ts +++ b/lib/ts/recipe/passwordless/smsdelivery/services/twilio/index.ts @@ -22,6 +22,7 @@ import Twilio from "twilio"; import OverrideableBuilder from "supertokens-js-override"; import { TypePasswordlessSmsDeliveryInput } from "../../../types"; import { getServiceImplementation } from "./serviceImplementation"; +import { UserContext } from "../../../../../types"; export default class TwilioService implements SmsDeliveryInterface { serviceImpl: ServiceInterface; @@ -41,7 +42,7 @@ export default class TwilioService implements SmsDeliveryInterface { + sendSms = async (input: TypePasswordlessSmsDeliveryInput & { userContext: UserContext }) => { let content = await this.serviceImpl.getContent(input); if ("from" in this.config.twilioSettings) { await this.serviceImpl.sendRawSms({ diff --git a/lib/ts/recipe/passwordless/smsdelivery/services/twilio/serviceImplementation.ts b/lib/ts/recipe/passwordless/smsdelivery/services/twilio/serviceImplementation.ts index 03931bbcd..64a7c4f8a 100644 --- a/lib/ts/recipe/passwordless/smsdelivery/services/twilio/serviceImplementation.ts +++ b/lib/ts/recipe/passwordless/smsdelivery/services/twilio/serviceImplementation.ts @@ -21,6 +21,7 @@ import { GetContentResult, } from "../../../../../ingredients/smsdelivery/services/twilio"; import getPasswordlessLoginSmsContent from "./passwordlessLogin"; +import { UserContext } from "../../../../../types"; export function getServiceImplementation(twilioClient: Twilio): ServiceInterface { return { @@ -40,7 +41,7 @@ export function getServiceImplementation(twilioClient: Twilio): ServiceInterface } }, getContent: async function ( - input: TypePasswordlessSmsDeliveryInput & { userContext: any } + input: TypePasswordlessSmsDeliveryInput & { userContext: UserContext } ): Promise { return getPasswordlessLoginSmsContent(input); }, diff --git a/lib/ts/recipe/passwordless/types.ts b/lib/ts/recipe/passwordless/types.ts index 0e16dbd15..b59bf68d8 100644 --- a/lib/ts/recipe/passwordless/types.ts +++ b/lib/ts/recipe/passwordless/types.ts @@ -26,8 +26,9 @@ import { TypeInputWithService as SmsDeliveryTypeInputWithService, } from "../../ingredients/smsdelivery/types"; import SmsDeliveryIngredient from "../../ingredients/smsdelivery"; -import { GeneralErrorResponse, NormalisedAppinfo, User } from "../../types"; +import { GeneralErrorResponse, NormalisedAppinfo, User, UserContext } from "../../types"; import RecipeUserId from "../../recipeUserId"; +import { MFAFlowErrors } from "../multifactorauth/types"; // As per https://github.com/supertokens/supertokens-core/issues/325 export type TypeInput = ( @@ -58,7 +59,7 @@ export type TypeInput = ( // Override this to override how user input codes are generated // By default (=undefined) it is done in the Core - getCustomUserInputCode?: (tenantId: string, userContext: any) => Promise | string; + getCustomUserInputCode?: (tenantId: string, userContext: UserContext) => Promise | string; override?: { functions?: ( @@ -94,7 +95,7 @@ export type TypeNormalisedInput = ( // Override this to override how user input codes are generated // By default (=undefined) it is done in the Core - getCustomUserInputCode?: (tenantId: string, userContext: any) => Promise | string; + getCustomUserInputCode?: (tenantId: string, userContext: UserContext) => Promise | string; getSmsDeliveryConfig: () => SmsDeliveryTypeInputWithService; getEmailDeliveryConfig: () => EmailDeliveryTypeInputWithService; @@ -116,7 +117,7 @@ export type RecipeInterface = { | { phoneNumber: string; } - ) & { userInputCode?: string; tenantId: string; userContext: any } + ) & { userInputCode?: string; tenantId: string; userContext: UserContext } ) => Promise<{ status: "OK"; preAuthSessionId: string; @@ -131,7 +132,7 @@ export type RecipeInterface = { deviceId: string; userInputCode?: string; tenantId: string; - userContext: any; + userContext: UserContext; }) => Promise< | { status: "OK"; @@ -152,13 +153,15 @@ export type RecipeInterface = { deviceId: string; preAuthSessionId: string; tenantId: string; - userContext: any; + shouldAttemptAccountLinkingIfAllowed?: boolean; + userContext: UserContext; } | { linkCode: string; preAuthSessionId: string; tenantId: string; - userContext: any; + shouldAttemptAccountLinkingIfAllowed?: boolean; + userContext: UserContext; } ) => Promise< | { @@ -166,7 +169,6 @@ export type RecipeInterface = { createdNewRecipeUser: boolean; user: User; recipeUserId: RecipeUserId; - isValidFirstFactorForTenant: boolean | undefined; } | { status: "INCORRECT_USER_INPUT_CODE_ERROR" | "EXPIRED_USER_INPUT_CODE_ERROR"; @@ -180,7 +182,7 @@ export type RecipeInterface = { recipeUserId: RecipeUserId; email?: string | null; phoneNumber?: string | null; - userContext: any; + userContext: UserContext; }) => Promise< | { status: @@ -200,12 +202,12 @@ export type RecipeInterface = { | { email: string; tenantId: string; - userContext: any; + userContext: UserContext; } | { phoneNumber: string; tenantId: string; - userContext: any; + userContext: UserContext; } ) => Promise<{ status: "OK"; @@ -214,29 +216,29 @@ export type RecipeInterface = { revokeCode: (input: { codeId: string; tenantId: string; - userContext: any; + userContext: UserContext; }) => Promise<{ status: "OK"; }>; - listCodesByEmail: (input: { email: string; tenantId: string; userContext: any }) => Promise; + listCodesByEmail: (input: { email: string; tenantId: string; userContext: UserContext }) => Promise; listCodesByPhoneNumber: (input: { phoneNumber: string; tenantId: string; - userContext: any; + userContext: UserContext; }) => Promise; listCodesByDeviceId: (input: { deviceId: string; tenantId: string; - userContext: any; + userContext: UserContext; }) => Promise; listCodesByPreAuthSessionId: (input: { preAuthSessionId: string; tenantId: string; - userContext: any; + userContext: UserContext; }) => Promise; }; @@ -272,7 +274,7 @@ export type APIInterface = { input: ({ email: string } | { phoneNumber: string }) & { tenantId: string; options: APIOptions; - userContext: any; + userContext: UserContext; } ) => Promise< | { @@ -292,7 +294,7 @@ export type APIInterface = { input: { deviceId: string; preAuthSessionId: string } & { tenantId: string; options: APIOptions; - userContext: any; + userContext: UserContext; } ) => Promise; @@ -310,7 +312,7 @@ export type APIInterface = { ) & { tenantId: string; options: APIOptions; - userContext: any; + userContext: UserContext; } ) => Promise< | { @@ -324,19 +326,20 @@ export type APIInterface = { failedCodeInputAttemptCount: number; maximumCodeInputAttempts: number; } - | GeneralErrorResponse | { status: "RESTART_FLOW_ERROR" } | { status: "SIGN_IN_UP_NOT_ALLOWED"; reason: string; } + | GeneralErrorResponse + | MFAFlowErrors >; emailExistsGET?: (input: { email: string; tenantId: string; options: APIOptions; - userContext: any; + userContext: UserContext; }) => Promise< | { status: "OK"; @@ -349,7 +352,7 @@ export type APIInterface = { phoneNumber: string; tenantId: string; options: APIOptions; - userContext: any; + userContext: UserContext; }) => Promise< | { status: "OK"; diff --git a/lib/ts/recipe/passwordless/utils.ts b/lib/ts/recipe/passwordless/utils.ts index 06dceaa20..6c935f9d3 100644 --- a/lib/ts/recipe/passwordless/utils.ts +++ b/lib/ts/recipe/passwordless/utils.ts @@ -19,6 +19,7 @@ import { NormalisedAppinfo } from "../../types"; import parsePhoneNumber from "libphonenumber-js/max"; import BackwardCompatibilityEmailService from "./emaildelivery/services/backwardCompatibility"; import BackwardCompatibilitySmsService from "./smsdelivery/services/backwardCompatibility"; +import { User } from "../../user"; export function validateAndNormaliseUserInput( _: Recipe, @@ -174,3 +175,26 @@ export function defaultValidateEmail(value: string): Promise return undefined; } + +export function isFactorSetupForUser(user: User, factorId: string) { + for (const loginMethod of user.loginMethods) { + if (loginMethod.email !== undefined) { + if (factorId == "otp-email") { + return true; + } + if (factorId == "link-email") { + return true; + } + } + + if (loginMethod.phoneNumber !== undefined) { + if (factorId == "otp-phone") { + return true; + } + if (factorId == "link-phone") { + return true; + } + } + } + return false; +} diff --git a/lib/ts/recipe/session/api/implementation.ts b/lib/ts/recipe/session/api/implementation.ts index 224ff5d36..1b0403e69 100644 --- a/lib/ts/recipe/session/api/implementation.ts +++ b/lib/ts/recipe/session/api/implementation.ts @@ -2,7 +2,7 @@ import { APIInterface, APIOptions, VerifySessionOptions } from "../"; import { normaliseHttpMethod } from "../../../utils"; import NormalisedURLPath from "../../../normalisedURLPath"; import { SessionContainerInterface } from "../types"; -import { GeneralErrorResponse } from "../../../types"; +import { GeneralErrorResponse, UserContext } from "../../../types"; import { getSessionFromRequest, refreshSessionInRequest } from "../sessionRequestFunctions"; export default function getAPIInterface(): APIInterface { @@ -12,7 +12,7 @@ export default function getAPIInterface(): APIInterface { userContext, }: { options: APIOptions; - userContext: any; + userContext: UserContext; }): Promise { return refreshSessionInRequest({ req: options.req, @@ -30,7 +30,7 @@ export default function getAPIInterface(): APIInterface { }: { verifySessionOptions: VerifySessionOptions | undefined; options: APIOptions; - userContext: any; + userContext: UserContext; }): Promise { let method = normaliseHttpMethod(options.req.getMethod()); if (method === "options" || method === "trace") { @@ -67,7 +67,7 @@ export default function getAPIInterface(): APIInterface { }: { options: APIOptions; session: SessionContainerInterface | undefined; - userContext: any; + userContext: UserContext; }): Promise< | { status: "OK"; diff --git a/lib/ts/recipe/session/api/refresh.ts b/lib/ts/recipe/session/api/refresh.ts index b1d782741..1f005a16e 100644 --- a/lib/ts/recipe/session/api/refresh.ts +++ b/lib/ts/recipe/session/api/refresh.ts @@ -15,11 +15,12 @@ import { send200Response } from "../../../utils"; import { APIInterface, APIOptions } from "../"; +import { UserContext } from "../../../types"; export default async function handleRefreshAPI( apiImplementation: APIInterface, options: APIOptions, - userContext: any + userContext: UserContext ): Promise { if (apiImplementation.refreshPOST === undefined) { return false; diff --git a/lib/ts/recipe/session/api/signout.ts b/lib/ts/recipe/session/api/signout.ts index 3db96a1e1..83d95edd7 100644 --- a/lib/ts/recipe/session/api/signout.ts +++ b/lib/ts/recipe/session/api/signout.ts @@ -16,11 +16,12 @@ import { send200Response } from "../../../utils"; import { APIInterface, APIOptions } from "../"; import { getSessionFromRequest } from "../sessionRequestFunctions"; +import { UserContext } from "../../../types"; export default async function signOutAPI( apiImplementation: APIInterface, options: APIOptions, - userContext: any + userContext: UserContext ): Promise { // Logic as per https://github.com/supertokens/supertokens-node/issues/34#issuecomment-717958537 diff --git a/lib/ts/recipe/session/claimBaseClasses/primitiveArrayClaim.ts b/lib/ts/recipe/session/claimBaseClasses/primitiveArrayClaim.ts index bf785d862..121b52015 100644 --- a/lib/ts/recipe/session/claimBaseClasses/primitiveArrayClaim.ts +++ b/lib/ts/recipe/session/claimBaseClasses/primitiveArrayClaim.ts @@ -1,5 +1,5 @@ import RecipeUserId from "../../../recipeUserId"; -import { JSONPrimitive } from "../../../types"; +import { JSONObject, JSONPrimitive, UserContext } from "../../../types"; import { SessionClaim, SessionClaimValidator } from "../types"; export class PrimitiveArrayClaim extends SessionClaim { @@ -7,7 +7,8 @@ export class PrimitiveArrayClaim extends SessionClaim Promise | T[] | undefined; public readonly defaultMaxAgeInSeconds: number | undefined; @@ -17,7 +18,7 @@ export class PrimitiveArrayClaim extends SessionClaim extends SessionClaim extends SessionClaim extends SessionClaim extends SessionClaim { @@ -7,7 +7,8 @@ export class PrimitiveClaim extends SessionClaim { userId: string, recipeUserId: RecipeUserId, tenantId: string, - userContext: any + currentPayload: JSONObject | undefined, + userContext: UserContext ) => Promise | T | undefined; public readonly defaultMaxAgeInSeconds: number | undefined; @@ -17,7 +18,7 @@ export class PrimitiveClaim extends SessionClaim { this.defaultMaxAgeInSeconds = config.defaultMaxAgeInSeconds; } - addToPayload_internal(payload: any, value: T, _userContext: any): any { + addToPayload_internal(payload: any, value: T, _userContext: UserContext): any { return { ...payload, [this.key]: { @@ -26,7 +27,7 @@ export class PrimitiveClaim extends SessionClaim { }, }; } - removeFromPayloadByMerge_internal(payload: any, _userContext?: any): any { + removeFromPayloadByMerge_internal(payload: any, _userContext?: UserContext): any { const res = { ...payload, [this.key]: null, @@ -35,7 +36,7 @@ export class PrimitiveClaim extends SessionClaim { return res; } - removeFromPayload(payload: any, _userContext?: any): any { + removeFromPayload(payload: any, _userContext?: UserContext): any { const res = { ...payload, }; @@ -44,11 +45,11 @@ export class PrimitiveClaim extends SessionClaim { return res; } - getValueFromPayload(payload: any, _userContext?: any): T | undefined { + getValueFromPayload(payload: any, _userContext?: UserContext): T | undefined { return payload[this.key]?.v; } - getLastRefetchTime(payload: any, _userContext?: any): number | undefined { + getLastRefetchTime(payload: any, _userContext?: UserContext): number | undefined { return payload[this.key]?.t; } diff --git a/lib/ts/recipe/session/cookieAndHeaders.ts b/lib/ts/recipe/session/cookieAndHeaders.ts index f30cdba0c..3d12c082c 100644 --- a/lib/ts/recipe/session/cookieAndHeaders.ts +++ b/lib/ts/recipe/session/cookieAndHeaders.ts @@ -15,6 +15,7 @@ import { HEADER_RID } from "../../constants"; import type { BaseRequest, BaseResponse } from "../../framework"; import { logDebugMessage } from "../../logger"; +import { UserContext } from "../../types"; import { availableTokenTransferMethods } from "./constants"; import { TokenTransferMethod, TokenType, TypeNormalisedInput } from "./types"; @@ -34,7 +35,7 @@ export function clearSessionFromAllTokenTransferMethods( config: TypeNormalisedInput, res: BaseResponse, request: BaseRequest | undefined, - userContext: any + userContext: UserContext ) { // We are clearing the session in all transfermethods to be sure to override cookies in case they have been already added to the response. // This is done to handle the following use-case: @@ -52,7 +53,7 @@ export function clearSession( res: BaseResponse, transferMethod: TokenTransferMethod, request: BaseRequest | undefined, - userContext: any + userContext: UserContext ) { // If we can be specific about which transferMethod we want to clear, there is no reason to clear the other ones const tokenTypes: TokenType[] = ["access", "refresh"]; @@ -138,7 +139,7 @@ export function setToken( expires: number, transferMethod: TokenTransferMethod, req: BaseRequest | undefined, - userContext: any + userContext: UserContext ) { logDebugMessage(`setToken: Setting ${tokenType} token as ${transferMethod}`); if (transferMethod === "cookie") { @@ -181,7 +182,7 @@ export function setCookie( expires: number, pathType: "refreshTokenPath" | "accessTokenPath", req: BaseRequest | undefined, - userContext: any + userContext: UserContext ) { let domain = config.cookieDomain; let secure = config.cookieSecure; diff --git a/lib/ts/recipe/session/index.ts b/lib/ts/recipe/session/index.ts index d207cc97f..e26222a3a 100644 --- a/lib/ts/recipe/session/index.ts +++ b/lib/ts/recipe/session/index.ts @@ -33,6 +33,7 @@ import RecipeUserId from "../../recipeUserId"; import { getUser } from "../.."; import { DEFAULT_TENANT_ID } from "../multitenancy/constants"; import { protectedProps } from "./constants"; +import { getUserContext } from "../../utils"; export default class SessionWrapper { static init = Recipe.init; @@ -46,7 +47,7 @@ export default class SessionWrapper { recipeUserId: RecipeUserId, accessTokenPayload: any = {}, sessionDataInDatabase: any = {}, - userContext: any = {} + userContext?: Record ) { const recipeInstance = Recipe.getInstanceOrThrowError(); const config = recipeInstance.config; @@ -61,7 +62,7 @@ export default class SessionWrapper { return await createNewSessionInRequest({ req, res, - userContext, + userContext: getUserContext(userContext), recipeInstance, accessTokenPayload, userId, @@ -73,14 +74,50 @@ export default class SessionWrapper { }); } + static async createNewOrKeepExistingSession( + req: any, + res: any, + tenantId: string, + recipeUserId: RecipeUserId, + accessTokenPayload: any = {}, + sessionDataInDatabase: any = {}, + userContext?: Record + ) { + const ctx = getUserContext(userContext); + const recipeInstance = Recipe.getInstanceOrThrowError(); + const config = recipeInstance.config; + + let session = await getSession( + req, + req, + { sessionRequired: false, overrideGlobalClaimValidators: () => [] }, + ctx + ); + + if (session === undefined || config.overwriteSessionDuringSignIn) { + session = await createNewSession( + req, + res, + tenantId, + recipeUserId, + accessTokenPayload, + sessionDataInDatabase, + ctx + ); + } + + return session; + } + static async createNewSessionWithoutRequestResponse( tenantId: string, recipeUserId: RecipeUserId, accessTokenPayload: any = {}, sessionDataInDatabase: any = {}, disableAntiCsrf: boolean = false, - userContext: any = {} + userContext?: Record ) { + const ctx = getUserContext(userContext); const recipeInstance = Recipe.getInstanceOrThrowError(); const claimsAddedByOtherRecipes = recipeInstance.getClaimsAddedByOtherRecipes(); const appInfo = recipeInstance.getAppInfo(); @@ -95,14 +132,14 @@ export default class SessionWrapper { delete finalAccessTokenPayload[prop]; } - let user = await getUser(recipeUserId.getAsString(), userContext); + let user = await getUser(recipeUserId.getAsString(), ctx); let userId = recipeUserId.getAsString(); if (user !== undefined) { userId = user.id; } for (const claim of claimsAddedByOtherRecipes) { - const update = await claim.build(userId, recipeUserId, tenantId, userContext); + const update = await claim.build(userId, recipeUserId, tenantId, finalAccessTokenPayload, ctx); finalAccessTokenPayload = { ...finalAccessTokenPayload, ...update, @@ -115,7 +152,7 @@ export default class SessionWrapper { sessionDataInDatabase, disableAntiCsrf, tenantId, - userContext, + userContext: ctx, }); } @@ -124,9 +161,9 @@ export default class SessionWrapper { overrideGlobalClaimValidators?: ( globalClaimValidators: SessionClaimValidator[], sessionInfo: SessionInformation, - userContext: any + userContext: Record ) => Promise | SessionClaimValidator[], - userContext: any = {} + userContext?: Record ): Promise< | { status: "SESSION_DOES_NOT_EXIST_ERROR"; @@ -136,11 +173,12 @@ export default class SessionWrapper { invalidClaims: ClaimValidationError[]; } > { + const ctx = getUserContext(userContext); const recipeImpl = Recipe.getInstanceOrThrowError().recipeInterfaceImpl; const sessionInfo = await recipeImpl.getSessionInformation({ sessionHandle, - userContext, + userContext: ctx, }); if (sessionInfo === undefined) { return { @@ -154,12 +192,12 @@ export default class SessionWrapper { recipeUserId: sessionInfo.recipeUserId, tenantId: sessionInfo.tenantId, claimValidatorsAddedByOtherRecipes, - userContext, + userContext: ctx, }); const claimValidators = overrideGlobalClaimValidators !== undefined - ? await overrideGlobalClaimValidators(globalClaimValidators, sessionInfo, userContext) + ? await overrideGlobalClaimValidators(globalClaimValidators, sessionInfo, ctx) : globalClaimValidators; let claimValidationResponse = await recipeImpl.validateClaims({ @@ -167,7 +205,7 @@ export default class SessionWrapper { recipeUserId: sessionInfo.recipeUserId, accessTokenPayload: sessionInfo.customClaimsInAccessTokenPayload, claimValidators, - userContext, + userContext: ctx, }); if (claimValidationResponse.accessTokenPayloadUpdate !== undefined) { @@ -175,7 +213,7 @@ export default class SessionWrapper { !(await recipeImpl.mergeIntoAccessTokenPayload({ sessionHandle, accessTokenPayloadUpdate: claimValidationResponse.accessTokenPayloadUpdate, - userContext, + userContext: ctx, })) ) { return { @@ -194,21 +232,21 @@ export default class SessionWrapper { req: any, res: any, options?: VerifySessionOptions & { sessionRequired?: true }, - userContext?: any + userContext?: Record ): Promise; static getSession( req: any, res: any, options?: VerifySessionOptions & { sessionRequired: false }, - userContext?: any + userContext?: Record ): Promise; static getSession( req: any, res: any, options?: VerifySessionOptions, - userContext?: any + userContext?: Record ): Promise; - static async getSession(req: any, res: any, options?: VerifySessionOptions, userContext?: any) { + static async getSession(req: any, res: any, options?: VerifySessionOptions, userContext?: Record) { const recipeInstance = Recipe.getInstanceOrThrowError(); const config = recipeInstance.config; const recipeInterfaceImpl = recipeInstance.recipeInterfaceImpl; @@ -219,7 +257,7 @@ export default class SessionWrapper { recipeInterfaceImpl, config, options, - userContext, // userContext is normalized inside the function + userContext: getUserContext(userContext), // userContext is normalized inside the function }); } @@ -251,85 +289,92 @@ export default class SessionWrapper { accessToken: string, antiCsrfToken?: string, options?: VerifySessionOptions & { sessionRequired?: true }, - userContext?: any + userContext?: Record ): Promise; static async getSessionWithoutRequestResponse( accessToken: string, antiCsrfToken?: string, options?: VerifySessionOptions & { sessionRequired: false }, - userContext?: any + userContext?: Record ): Promise; static async getSessionWithoutRequestResponse( accessToken: string, antiCsrfToken?: string, options?: VerifySessionOptions, - userContext?: any + userContext?: Record ): Promise; static async getSessionWithoutRequestResponse( accessToken: string, antiCsrfToken?: string, options?: VerifySessionOptions, - userContext: any = {} + userContext?: Record ): Promise { + const ctx = getUserContext(userContext); const recipeInterfaceImpl = Recipe.getInstanceOrThrowError().recipeInterfaceImpl; const session = await recipeInterfaceImpl.getSession({ accessToken, antiCsrfToken, options, - userContext, + userContext: ctx, }); if (session !== undefined) { const claimValidators = await getRequiredClaimValidators( session, options?.overrideGlobalClaimValidators, - userContext + ctx ); - await session.assertClaims(claimValidators, userContext); + await session.assertClaims(claimValidators, ctx); } return session; } - static getSessionInformation(sessionHandle: string, userContext: any = {}) { + static getSessionInformation(sessionHandle: string, userContext?: Record) { return Recipe.getInstanceOrThrowError().recipeInterfaceImpl.getSessionInformation({ sessionHandle, - userContext, + userContext: getUserContext(userContext), }); } - static refreshSession(req: any, res: any, userContext: any = {}) { + static refreshSession(req: any, res: any, userContext?: Record) { const recipeInstance = Recipe.getInstanceOrThrowError(); const config = recipeInstance.config; const recipeInterfaceImpl = recipeInstance.recipeInterfaceImpl; - return refreshSessionInRequest({ res, req, userContext, config, recipeInterfaceImpl }); + return refreshSessionInRequest({ + res, + req, + userContext: getUserContext(userContext), + config, + recipeInterfaceImpl, + }); } static refreshSessionWithoutRequestResponse( refreshToken: string, disableAntiCsrf: boolean = false, antiCsrfToken?: string, - userContext: any = {} + userContext?: Record ) { return Recipe.getInstanceOrThrowError().recipeInterfaceImpl.refreshSession({ refreshToken, disableAntiCsrf, antiCsrfToken, - userContext, + userContext: getUserContext(userContext), }); } static revokeAllSessionsForUser( userId: string, revokeSessionsForLinkedAccounts: boolean = true, tenantId?: string, - userContext: any = {} + userContext?: Record ) { return Recipe.getInstanceOrThrowError().recipeInterfaceImpl.revokeAllSessionsForUser({ userId, tenantId: tenantId === undefined ? DEFAULT_TENANT_ID : tenantId, revokeSessionsForLinkedAccounts, - userContext, + userContext: getUserContext(userContext), }); } @@ -337,75 +382,86 @@ export default class SessionWrapper { userId: string, fetchSessionsForAllLinkedAccounts: boolean = true, tenantId?: string, - userContext: any = {} + userContext?: Record ) { return Recipe.getInstanceOrThrowError().recipeInterfaceImpl.getAllSessionHandlesForUser({ userId, tenantId: tenantId === undefined ? DEFAULT_TENANT_ID : tenantId, fetchAcrossAllTenants: tenantId === undefined, fetchSessionsForAllLinkedAccounts, - userContext, + userContext: getUserContext(userContext), }); } - static revokeSession(sessionHandle: string, userContext: any = {}) { + static revokeSession(sessionHandle: string, userContext?: Record) { return Recipe.getInstanceOrThrowError().recipeInterfaceImpl.revokeSession({ sessionHandle, - userContext, + userContext: getUserContext(userContext), }); } - static revokeMultipleSessions(sessionHandles: string[], userContext: any = {}) { + static revokeMultipleSessions(sessionHandles: string[], userContext?: Record) { return Recipe.getInstanceOrThrowError().recipeInterfaceImpl.revokeMultipleSessions({ sessionHandles, - userContext, + userContext: getUserContext(userContext), }); } - static updateSessionDataInDatabase(sessionHandle: string, newSessionData: any, userContext: any = {}) { + static updateSessionDataInDatabase(sessionHandle: string, newSessionData: any, userContext?: Record) { return Recipe.getInstanceOrThrowError().recipeInterfaceImpl.updateSessionDataInDatabase({ sessionHandle, newSessionData, - userContext, + userContext: getUserContext(userContext), }); } static mergeIntoAccessTokenPayload( sessionHandle: string, accessTokenPayloadUpdate: JSONObject, - userContext: any = {} + userContext?: Record ) { return Recipe.getInstanceOrThrowError().recipeInterfaceImpl.mergeIntoAccessTokenPayload({ sessionHandle, accessTokenPayloadUpdate, - userContext, + userContext: getUserContext(userContext), }); } - static createJWT(payload?: any, validitySeconds?: number, useStaticSigningKey?: boolean, userContext: any = {}) { + static createJWT( + payload?: any, + validitySeconds?: number, + useStaticSigningKey?: boolean, + userContext?: Record + ) { return Recipe.getInstanceOrThrowError().openIdRecipe.recipeImplementation.createJWT({ payload, validitySeconds, useStaticSigningKey, - userContext, + userContext: getUserContext(userContext), }); } - static getJWKS(userContext: any = {}) { - return Recipe.getInstanceOrThrowError().openIdRecipe.recipeImplementation.getJWKS({ userContext }); + static getJWKS(userContext?: Record) { + return Recipe.getInstanceOrThrowError().openIdRecipe.recipeImplementation.getJWKS({ + userContext: getUserContext(userContext), + }); } - static getOpenIdDiscoveryConfiguration(userContext: any = {}) { + static getOpenIdDiscoveryConfiguration(userContext?: Record) { return Recipe.getInstanceOrThrowError().openIdRecipe.recipeImplementation.getOpenIdDiscoveryConfiguration({ - userContext, + userContext: getUserContext(userContext), }); } - static fetchAndSetClaim(sessionHandle: string, claim: SessionClaim, userContext: any = {}): Promise { + static fetchAndSetClaim( + sessionHandle: string, + claim: SessionClaim, + userContext?: Record + ): Promise { return Recipe.getInstanceOrThrowError().recipeInterfaceImpl.fetchAndSetClaim({ sessionHandle, claim, - userContext, + userContext: getUserContext(userContext), }); } @@ -413,20 +469,20 @@ export default class SessionWrapper { sessionHandle: string, claim: SessionClaim, value: T, - userContext: any = {} + userContext?: Record ): Promise { return Recipe.getInstanceOrThrowError().recipeInterfaceImpl.setClaimValue({ sessionHandle, claim, value, - userContext, + userContext: getUserContext(userContext), }); } static getClaimValue( sessionHandle: string, claim: SessionClaim, - userContext: any = {} + userContext?: Record ): Promise< | { status: "SESSION_DOES_NOT_EXIST_ERROR"; @@ -439,15 +495,19 @@ export default class SessionWrapper { return Recipe.getInstanceOrThrowError().recipeInterfaceImpl.getClaimValue({ sessionHandle, claim, - userContext, + userContext: getUserContext(userContext), }); } - static removeClaim(sessionHandle: string, claim: SessionClaim, userContext: any = {}): Promise { + static removeClaim( + sessionHandle: string, + claim: SessionClaim, + userContext?: Record + ): Promise { return Recipe.getInstanceOrThrowError().recipeInterfaceImpl.removeClaim({ sessionHandle, claim, - userContext, + userContext: getUserContext(userContext), }); } } diff --git a/lib/ts/recipe/session/recipe.ts b/lib/ts/recipe/session/recipe.ts index 21e471548..4bf91fdb4 100644 --- a/lib/ts/recipe/session/recipe.ts +++ b/lib/ts/recipe/session/recipe.ts @@ -25,7 +25,7 @@ import { } from "./types"; import STError from "./error"; import { validateAndNormaliseUserInput } from "./utils"; -import { NormalisedAppinfo, RecipeListFunction, APIHandled, HTTPMethod } from "../../types"; +import { NormalisedAppinfo, RecipeListFunction, APIHandled, HTTPMethod, UserContext } from "../../types"; import handleRefreshAPI from "./api/refresh"; import signOutAPI from "./api/signout"; import { REFRESH_API_PATH, SIGNOUT_API_PATH } from "./constants"; @@ -179,7 +179,7 @@ export default class SessionRecipe extends RecipeModule { res: BaseResponse, path: NormalisedURLPath, method: HTTPMethod, - userContext: any + userContext: UserContext ): Promise => { let options: APIOptions = { config: this.config, @@ -198,7 +198,7 @@ export default class SessionRecipe extends RecipeModule { } }; - handleError = async (err: STError, request: BaseRequest, response: BaseResponse, userContext: any) => { + handleError = async (err: STError, request: BaseRequest, response: BaseResponse, userContext: UserContext) => { if (err.fromRecipe === SessionRecipe.RECIPE_ID) { if (err.type === STError.UNAUTHORISED) { logDebugMessage("errorHandler: returning UNAUTHORISED"); @@ -210,10 +210,10 @@ export default class SessionRecipe extends RecipeModule { logDebugMessage("errorHandler: Clearing tokens because of UNAUTHORISED response"); clearSessionFromAllTokenTransferMethods(this.config, response, request, userContext); } - return await this.config.errorHandlers.onUnauthorised(err.message, request, response); + return await this.config.errorHandlers.onUnauthorised(err.message, request, response, userContext); } else if (err.type === STError.TRY_REFRESH_TOKEN) { logDebugMessage("errorHandler: returning TRY_REFRESH_TOKEN"); - return await this.config.errorHandlers.onTryRefreshToken(err.message, request, response); + return await this.config.errorHandlers.onTryRefreshToken(err.message, request, response, userContext); } else if (err.type === STError.TOKEN_THEFT_DETECTED) { logDebugMessage("errorHandler: returning TOKEN_THEFT_DETECTED"); logDebugMessage("errorHandler: Clearing tokens because of TOKEN_THEFT_DETECTED response"); @@ -223,15 +223,16 @@ export default class SessionRecipe extends RecipeModule { err.payload.userId, err.payload.recipeUserId, request, - response + response, + userContext ); } else if (err.type === STError.INVALID_CLAIMS) { - return await this.config.errorHandlers.onInvalidClaim(err.payload, request, response); + return await this.config.errorHandlers.onInvalidClaim(err.payload, request, response, userContext); } else { throw err; } } else { - return await this.openIdRecipe.handleError(err, request, response); + return await this.openIdRecipe.handleError(err, request, response, userContext); } }; @@ -254,7 +255,7 @@ export default class SessionRecipe extends RecipeModule { options: VerifySessionOptions | undefined, request: BaseRequest, response: BaseResponse, - userContext: any + userContext: UserContext ) => { return await this.apiImpl.verifySession({ verifySessionOptions: options, diff --git a/lib/ts/recipe/session/recipeImplementation.ts b/lib/ts/recipe/session/recipeImplementation.ts index a2dbaa981..d01efc8ca 100644 --- a/lib/ts/recipe/session/recipeImplementation.ts +++ b/lib/ts/recipe/session/recipeImplementation.ts @@ -15,7 +15,7 @@ import { validateClaimsInPayload } from "./utils"; import Session from "./sessionClass"; import { Querier } from "../../querier"; import NormalisedURLPath from "../../normalisedURLPath"; -import { JSONObject, NormalisedAppinfo } from "../../types"; +import { JSONObject, NormalisedAppinfo, UserContext } from "../../types"; import { logDebugMessage } from "../../logger"; import { ParsedJWTInfo, parseJWTWithoutSignatureVerification } from "./jwt"; import { validateAccessTokenStructure } from "./accessToken"; @@ -87,7 +87,7 @@ export default function getRecipeInterface( accessTokenPayload?: any; sessionDataInDatabase?: any; tenantId: string; - userContext: any; + userContext: UserContext; }): Promise { logDebugMessage("createNewSession: Started"); @@ -134,7 +134,7 @@ export default function getRecipeInterface( accessToken: string; antiCsrfToken?: string; options?: VerifySessionOptions; - userContext: any; + userContext: UserContext; }): Promise { if ( options?.antiCsrfCheck !== false && @@ -235,7 +235,7 @@ export default function getRecipeInterface( recipeUserId: RecipeUserId; accessTokenPayload: any; claimValidators: SessionClaimValidator[]; - userContext: any; + userContext: UserContext; } ): Promise<{ invalidClaims: ClaimValidationError[]; @@ -253,6 +253,7 @@ export default function getRecipeInterface( input.userId, input.recipeUserId, accessTokenPayload.tId === undefined ? DEFAULT_TENANT_ID : accessTokenPayload.tId, + accessTokenPayload, input.userContext ); logDebugMessage( @@ -289,7 +290,7 @@ export default function getRecipeInterface( userContext, }: { sessionHandle: string; - userContext: any; + userContext: UserContext; }): Promise { return SessionFunctions.getSessionInformation(helpers, sessionHandle, userContext); }, @@ -301,7 +302,12 @@ export default function getRecipeInterface( antiCsrfToken, disableAntiCsrf, userContext, - }: { refreshToken: string; antiCsrfToken?: string; disableAntiCsrf: boolean; userContext: any } + }: { + refreshToken: string; + antiCsrfToken?: string; + disableAntiCsrf: boolean; + userContext: UserContext; + } ): Promise { if ( disableAntiCsrf !== true && @@ -346,7 +352,7 @@ export default function getRecipeInterface( input: { accessToken: string; newAccessTokenPayload?: any; - userContext: any; + userContext: UserContext; } ): Promise< | { @@ -406,7 +412,7 @@ export default function getRecipeInterface( revokeSessionsForLinkedAccounts: boolean; tenantId?: string; revokeAcrossAllTenants?: boolean; - userContext: any; + userContext: UserContext; }) { return SessionFunctions.revokeAllSessionsForUser( helpers, @@ -429,7 +435,7 @@ export default function getRecipeInterface( fetchSessionsForAllLinkedAccounts: boolean; tenantId?: string; fetchAcrossAllTenants?: boolean; - userContext: any; + userContext: UserContext; }): Promise { return SessionFunctions.getAllSessionHandlesForUser( helpers, @@ -446,7 +452,7 @@ export default function getRecipeInterface( userContext, }: { sessionHandle: string; - userContext: any; + userContext: UserContext; }): Promise { return SessionFunctions.revokeSession(helpers, sessionHandle, userContext); }, @@ -456,7 +462,7 @@ export default function getRecipeInterface( userContext, }: { sessionHandles: string[]; - userContext: any; + userContext: UserContext; }) { return SessionFunctions.revokeMultipleSessions(helpers, sessionHandles, userContext); }, @@ -468,7 +474,7 @@ export default function getRecipeInterface( }: { sessionHandle: string; newSessionData: any; - userContext: any; + userContext: UserContext; }): Promise { return SessionFunctions.updateSessionDataInDatabase(helpers, sessionHandle, newSessionData, userContext); }, @@ -482,7 +488,7 @@ export default function getRecipeInterface( }: { sessionHandle: string; accessTokenPayloadUpdate: JSONObject; - userContext: any; + userContext: UserContext; } ) { const sessionInfo = await this.getSessionInformation({ sessionHandle, userContext }); @@ -514,7 +520,7 @@ export default function getRecipeInterface( input: { sessionHandle: string; claim: SessionClaim; - userContext?: any; + userContext: UserContext; } ) { const sessionInfo = await this.getSessionInformation({ @@ -528,6 +534,7 @@ export default function getRecipeInterface( sessionInfo.userId, sessionInfo.recipeUserId, sessionInfo.tenantId, + sessionInfo.customClaimsInAccessTokenPayload, input.userContext ); @@ -544,7 +551,7 @@ export default function getRecipeInterface( sessionHandle: string; claim: SessionClaim; value: T; - userContext?: any; + userContext: UserContext; } ) { const accessTokenPayloadUpdate = input.claim.addToPayload_internal({}, input.value, input.userContext); @@ -557,7 +564,7 @@ export default function getRecipeInterface( getClaimValue: async function ( this: RecipeInterface, - input: { sessionHandle: string; claim: SessionClaim; userContext?: any } + input: { sessionHandle: string; claim: SessionClaim; userContext: UserContext } ) { const sessionInfo = await this.getSessionInformation({ sessionHandle: input.sessionHandle, @@ -578,7 +585,7 @@ export default function getRecipeInterface( removeClaim: function ( this: RecipeInterface, - input: { sessionHandle: string; claim: SessionClaim; userContext?: any } + input: { sessionHandle: string; claim: SessionClaim; userContext: UserContext } ) { const accessTokenPayloadUpdate = input.claim.removeFromPayloadByMerge_internal({}, input.userContext); diff --git a/lib/ts/recipe/session/sessionClass.ts b/lib/ts/recipe/session/sessionClass.ts index fcbb9c8cf..31efc8fa5 100644 --- a/lib/ts/recipe/session/sessionClass.ts +++ b/lib/ts/recipe/session/sessionClass.ts @@ -21,7 +21,7 @@ import { parseJWTWithoutSignatureVerification } from "./jwt"; import { logDebugMessage } from "../../logger"; import RecipeUserId from "../../recipeUserId"; import { protectedProps } from "./constants"; -import { makeDefaultUserContextFromAPI } from "../../utils"; +import { getUserContext, makeDefaultUserContextFromAPI } from "../../utils"; export default class Session implements SessionContainerInterface { constructor( @@ -39,14 +39,19 @@ export default class Session implements SessionContainerInterface { protected tenantId: string ) {} - getRecipeUserId(_userContext?: any): RecipeUserId { + getRecipeUserId(_userContext?: Record): RecipeUserId { return this.recipeUserId; } - async revokeSession(userContext?: any) { + async revokeSession(userContext?: Record) { + const ctx = + userContext === undefined && this.reqResInfo !== undefined + ? makeDefaultUserContextFromAPI(this.reqResInfo.req) + : getUserContext(userContext); + await this.helpers.getRecipeImpl().revokeSession({ sessionHandle: this.sessionHandle, - userContext: userContext === undefined ? {} : userContext, + userContext: ctx, }); if (this.reqResInfo !== undefined) { @@ -61,15 +66,15 @@ export default class Session implements SessionContainerInterface { this.reqResInfo.res, this.reqResInfo.transferMethod, this.reqResInfo.req, - userContext === undefined ? makeDefaultUserContextFromAPI(this.reqResInfo.req) : userContext + ctx ); } } - async getSessionDataFromDatabase(userContext?: any): Promise { + async getSessionDataFromDatabase(userContext?: Record): Promise { let sessionInfo = await this.helpers.getRecipeImpl().getSessionInformation({ sessionHandle: this.sessionHandle, - userContext: userContext === undefined ? {} : userContext, + userContext: getUserContext(userContext), }); if (sessionInfo === undefined) { logDebugMessage("getSessionDataFromDatabase: Throwing UNAUTHORISED because session does not exist anymore"); @@ -81,12 +86,12 @@ export default class Session implements SessionContainerInterface { return sessionInfo.sessionDataInDatabase; } - async updateSessionDataInDatabase(newSessionData: any, userContext?: any) { + async updateSessionDataInDatabase(newSessionData: any, userContext?: Record) { if ( !(await this.helpers.getRecipeImpl().updateSessionDataInDatabase({ sessionHandle: this.sessionHandle, newSessionData, - userContext: userContext === undefined ? {} : userContext, + userContext: getUserContext(userContext), })) ) { logDebugMessage( @@ -99,15 +104,15 @@ export default class Session implements SessionContainerInterface { } } - getUserId(_userContext?: any) { + getUserId(_userContext?: Record) { return this.userId; } - getTenantId(_userContext?: any) { + getTenantId(_userContext?: Record) { return this.tenantId; } - getAccessTokenPayload(_userContext?: any) { + getAccessTokenPayload(_userContext?: Record) { return this.userDataInAccessToken; } @@ -130,8 +135,13 @@ export default class Session implements SessionContainerInterface { } // Any update to this function should also be reflected in the respective JWT version - async mergeIntoAccessTokenPayload(accessTokenPayloadUpdate: any, userContext?: any): Promise { - let newAccessTokenPayload = { ...this.getAccessTokenPayload(userContext) }; + async mergeIntoAccessTokenPayload(accessTokenPayloadUpdate: any, userContext?: Record): Promise { + const ctx = + userContext === undefined && this.reqResInfo !== undefined + ? makeDefaultUserContextFromAPI(this.reqResInfo.req) + : getUserContext(userContext); + + let newAccessTokenPayload = { ...this.getAccessTokenPayload(ctx) }; for (const key of protectedProps) { delete newAccessTokenPayload[key]; } @@ -147,7 +157,7 @@ export default class Session implements SessionContainerInterface { let response = await this.helpers.getRecipeImpl().regenerateAccessToken({ accessToken: this.getAccessToken(), newAccessTokenPayload, - userContext: userContext === undefined ? {} : userContext, + userContext: ctx, }); if (response === undefined) { @@ -176,7 +186,7 @@ export default class Session implements SessionContainerInterface { this.helpers.config, this.reqResInfo.transferMethod, this.reqResInfo.req, - userContext === undefined ? makeDefaultUserContextFromAPI(this.reqResInfo.req) : userContext + ctx ); } } else { @@ -184,16 +194,16 @@ export default class Session implements SessionContainerInterface { // We can't update the access token on the FE, as it will need to call refresh anyway but we handle this as a successful update during this request. // the changes will be reflected on the FE after refresh is called this.userDataInAccessToken = { - ...this.getAccessTokenPayload(), + ...this.getAccessTokenPayload(ctx), ...response.session.userDataInJWT, }; } } - async getTimeCreated(userContext?: any): Promise { + async getTimeCreated(userContext?: Record): Promise { let sessionInfo = await this.helpers.getRecipeImpl().getSessionInformation({ sessionHandle: this.sessionHandle, - userContext: userContext === undefined ? {} : userContext, + userContext: getUserContext(userContext), }); if (sessionInfo === undefined) { logDebugMessage("getTimeCreated: Throwing UNAUTHORISED because session does not exist anymore"); @@ -205,10 +215,10 @@ export default class Session implements SessionContainerInterface { return sessionInfo.timeCreated; } - async getExpiry(userContext?: any): Promise { + async getExpiry(userContext?: Record): Promise { let sessionInfo = await this.helpers.getRecipeImpl().getSessionInformation({ sessionHandle: this.sessionHandle, - userContext: userContext === undefined ? {} : userContext, + userContext: getUserContext(userContext), }); if (sessionInfo === undefined) { logDebugMessage("getExpiry: Throwing UNAUTHORISED because session does not exist anymore"); @@ -221,13 +231,14 @@ export default class Session implements SessionContainerInterface { } // Any update to this function should also be reflected in the respective JWT version - async assertClaims(claimValidators: SessionClaimValidator[], userContext?: any): Promise { + async assertClaims(claimValidators: SessionClaimValidator[], userContext?: Record): Promise { + const ctx = getUserContext(userContext); let validateClaimResponse = await this.helpers.getRecipeImpl().validateClaims({ - accessTokenPayload: this.getAccessTokenPayload(userContext), - userId: this.getUserId(userContext), - recipeUserId: this.getRecipeUserId(userContext), + accessTokenPayload: this.getAccessTokenPayload(ctx), + userId: this.getUserId(ctx), + recipeUserId: this.getRecipeUserId(ctx), claimValidators, - userContext, + userContext: ctx, }); if (validateClaimResponse.accessTokenPayloadUpdate !== undefined) { @@ -235,7 +246,7 @@ export default class Session implements SessionContainerInterface { delete validateClaimResponse.accessTokenPayloadUpdate[key]; } - await this.mergeIntoAccessTokenPayload(validateClaimResponse.accessTokenPayloadUpdate, userContext); + await this.mergeIntoAccessTokenPayload(validateClaimResponse.accessTokenPayloadUpdate, ctx); } if (validateClaimResponse.invalidClaims.length !== 0) { @@ -248,38 +259,45 @@ export default class Session implements SessionContainerInterface { } // Any update to this function should also be reflected in the respective JWT version - async fetchAndSetClaim(claim: SessionClaim, userContext?: any): Promise { + async fetchAndSetClaim(claim: SessionClaim, userContext?: Record): Promise { + const ctx = getUserContext(userContext); const update = await claim.build( - this.getUserId(userContext), - this.getRecipeUserId(userContext), - this.getTenantId(), - userContext + this.getUserId(ctx), + this.getRecipeUserId(ctx), + this.getTenantId(ctx), + this.getAccessTokenPayload(ctx), + ctx ); - return this.mergeIntoAccessTokenPayload(update, userContext); + return this.mergeIntoAccessTokenPayload(update, ctx); } // Any update to this function should also be reflected in the respective JWT version - setClaimValue(claim: SessionClaim, value: T, userContext?: any): Promise { - const update = claim.addToPayload_internal({}, value, userContext); - return this.mergeIntoAccessTokenPayload(update, userContext); + setClaimValue(claim: SessionClaim, value: T, userContext?: Record): Promise { + const ctx = getUserContext(userContext); + const update = claim.addToPayload_internal({}, value, getUserContext(ctx)); + return this.mergeIntoAccessTokenPayload(update, ctx); } // Any update to this function should also be reflected in the respective JWT version - async getClaimValue(claim: SessionClaim, userContext?: any) { - return claim.getValueFromPayload(await this.getAccessTokenPayload(userContext), userContext); + async getClaimValue(claim: SessionClaim, userContext?: Record) { + const ctx = getUserContext(userContext); + return claim.getValueFromPayload(await this.getAccessTokenPayload(ctx), ctx); } // Any update to this function should also be reflected in the respective JWT version - removeClaim(claim: SessionClaim, userContext?: any): Promise { - const update = claim.removeFromPayloadByMerge_internal({}, userContext); - return this.mergeIntoAccessTokenPayload(update, userContext); + removeClaim(claim: SessionClaim, userContext?: Record): Promise { + const ctx = getUserContext(userContext); + const update = claim.removeFromPayloadByMerge_internal({}, ctx); + return this.mergeIntoAccessTokenPayload(update, ctx); } - attachToRequestResponse(info: ReqResInfo, userContext?: any) { + attachToRequestResponse(info: ReqResInfo, userContext?: Record) { this.reqResInfo = info; if (this.accessTokenUpdated) { const { res, transferMethod } = info; + const ctx = + userContext !== undefined ? getUserContext(userContext) : makeDefaultUserContextFromAPI(info.req); setAccessTokenInResponse( res, @@ -288,7 +306,7 @@ export default class Session implements SessionContainerInterface { this.helpers.config, transferMethod, info.req, - userContext !== undefined ? userContext : makeDefaultUserContextFromAPI(info.req) + ctx ); if (this.refreshToken !== undefined) { setToken( @@ -299,7 +317,7 @@ export default class Session implements SessionContainerInterface { this.refreshToken.expiry, transferMethod, info.req, - userContext !== undefined ? userContext : makeDefaultUserContextFromAPI(info.req) + ctx ); } if (this.antiCsrfToken !== undefined) { diff --git a/lib/ts/recipe/session/sessionFunctions.ts b/lib/ts/recipe/session/sessionFunctions.ts index 4864d6e87..b6991455e 100644 --- a/lib/ts/recipe/session/sessionFunctions.ts +++ b/lib/ts/recipe/session/sessionFunctions.ts @@ -24,6 +24,7 @@ import { logDebugMessage } from "../../logger"; import RecipeUserId from "../../recipeUserId"; import { DEFAULT_TENANT_ID } from "../multitenancy/constants"; import { JWKCacheMaxAgeInMs } from "./constants"; +import { UserContext } from "../../types"; /** * @description call this to "login" a user. @@ -33,9 +34,9 @@ export async function createNewSession( tenantId: string, recipeUserId: RecipeUserId, disableAntiCsrf: boolean, - accessTokenPayload: any, - sessionDataInDatabase: any, - userContext: any + accessTokenPayload: any = {}, + sessionDataInDatabase: any = {}, + userContext: UserContext ): Promise { accessTokenPayload = accessTokenPayload === null || accessTokenPayload === undefined ? {} : accessTokenPayload; sessionDataInDatabase = @@ -86,7 +87,7 @@ export async function getSession( antiCsrfToken: string | undefined, doAntiCsrfCheck: boolean, alwaysCheckCore: boolean, - userContext: any + userContext: UserContext ): Promise<{ session: { handle: string; @@ -283,7 +284,7 @@ export async function getSession( export async function getSessionInformation( helpers: Helpers, sessionHandle: string, - userContext: any + userContext: UserContext ): Promise { let apiVersion = await helpers.querier.getAPIVersion(); @@ -324,7 +325,7 @@ export async function refreshSession( refreshToken: string, antiCsrfToken: string | undefined, disableAntiCsrf: boolean, - userContext: any + userContext: UserContext ): Promise { let requestBody: { refreshToken: string; @@ -403,7 +404,7 @@ export async function revokeAllSessionsForUser( revokeSessionsForLinkedAccounts: boolean, tenantId: string | undefined, revokeAcrossAllTenants: boolean | undefined, - userContext: any + userContext: UserContext ): Promise { if (tenantId === undefined) { tenantId = DEFAULT_TENANT_ID; @@ -429,7 +430,7 @@ export async function getAllSessionHandlesForUser( fetchSessionsForAllLinkedAccounts: boolean, tenantId: string | undefined, fetchAcrossAllTenants: boolean | undefined, - userContext: any + userContext: UserContext ): Promise { if (tenantId === undefined) { tenantId = DEFAULT_TENANT_ID; @@ -450,7 +451,11 @@ export async function getAllSessionHandlesForUser( * @description call to destroy one session * @returns true if session was deleted from db. Else false in case there was nothing to delete */ -export async function revokeSession(helpers: Helpers, sessionHandle: string, userContext: any): Promise { +export async function revokeSession( + helpers: Helpers, + sessionHandle: string, + userContext: UserContext +): Promise { let response = await helpers.querier.sendPostRequest( new NormalisedURLPath("/recipe/session/remove"), { @@ -468,7 +473,7 @@ export async function revokeSession(helpers: Helpers, sessionHandle: string, use export async function revokeMultipleSessions( helpers: Helpers, sessionHandles: string[], - userContext: any + userContext: UserContext ): Promise { let response = await helpers.querier.sendPostRequest( new NormalisedURLPath(`/recipe/session/remove`), @@ -487,7 +492,7 @@ export async function updateSessionDataInDatabase( helpers: Helpers, sessionHandle: string, newSessionData: any, - userContext: any + userContext: UserContext ): Promise { newSessionData = newSessionData === null || newSessionData === undefined ? {} : newSessionData; let response = await helpers.querier.sendPutRequest( @@ -508,7 +513,7 @@ export async function updateAccessTokenPayload( helpers: Helpers, sessionHandle: string, newAccessTokenPayload: any, - userContext: any + userContext: UserContext ): Promise { newAccessTokenPayload = newAccessTokenPayload === null || newAccessTokenPayload === undefined ? {} : newAccessTokenPayload; diff --git a/lib/ts/recipe/session/sessionRequestFunctions.ts b/lib/ts/recipe/session/sessionRequestFunctions.ts index 0f58a948e..9993ce5d0 100644 --- a/lib/ts/recipe/session/sessionRequestFunctions.ts +++ b/lib/ts/recipe/session/sessionRequestFunctions.ts @@ -15,7 +15,7 @@ import { availableTokenTransferMethods, protectedProps } from "./constants"; import { clearSession, getAntiCsrfTokenFromHeaders, getToken, setCookie } from "./cookieAndHeaders"; import { ParsedJWTInfo, parseJWTWithoutSignatureVerification } from "./jwt"; import { validateAccessTokenStructure } from "./accessToken"; -import { NormalisedAppinfo } from "../../types"; +import { NormalisedAppinfo, UserContext } from "../../types"; import SessionError from "./error"; import RecipeUserId from "../../recipeUserId"; @@ -35,7 +35,7 @@ export async function getSessionFromRequest({ config: TypeNormalisedInput; recipeInterfaceImpl: RecipeInterface; options?: VerifySessionOptions; - userContext?: any; + userContext?: UserContext; }): Promise { logDebugMessage("getSession: Started"); const configuredFramework = SuperTokens.getInstanceOrThrowError().framework; @@ -201,7 +201,7 @@ export async function refreshSessionInRequest({ }: { res: any; req: any; - userContext: any; + userContext: UserContext; config: TypeNormalisedInput; recipeInterfaceImpl: RecipeInterface; }) { @@ -362,7 +362,7 @@ export async function createNewSessionInRequest({ }: { req: any; res: any; - userContext: any; + userContext: UserContext; recipeInstance: Recipe; accessTokenPayload: any; userId: string; @@ -400,7 +400,7 @@ export async function createNewSessionInRequest({ } for (const claim of claimsAddedByOtherRecipes) { - const update = await claim.build(userId, recipeUserId, tenantId, userContext); + const update = await claim.build(userId, recipeUserId, tenantId, finalAccessTokenPayload, userContext); finalAccessTokenPayload = { ...finalAccessTokenPayload, ...update, diff --git a/lib/ts/recipe/session/types.ts b/lib/ts/recipe/session/types.ts index b3502fb59..b4828294c 100644 --- a/lib/ts/recipe/session/types.ts +++ b/lib/ts/recipe/session/types.ts @@ -17,7 +17,7 @@ import NormalisedURLPath from "../../normalisedURLPath"; import { RecipeInterface as JWTRecipeInterface, APIInterface as JWTAPIInterface } from "../jwt/types"; import OverrideableBuilder from "supertokens-js-override"; import { RecipeInterface as OpenIdRecipeInterface, APIInterface as OpenIdAPIInterface } from "../openid/types"; -import { JSONObject, JSONValue } from "../../types"; +import { JSONObject, JSONValue, UserContext } from "../../types"; import { GeneralErrorResponse } from "../../types"; import RecipeUserId from "../../recipeUserId"; @@ -67,11 +67,12 @@ export type TypeInput = { cookieSecure?: boolean; cookieSameSite?: "strict" | "lax" | "none"; cookieDomain?: string; + overwriteSessionDuringSignIn?: boolean; getTokenTransferMethod?: (input: { req: BaseRequest; forCreateNewSession: boolean; - userContext: any; + userContext: UserContext; }) => TokenTransferMethod | "any"; errorHandlers?: ErrorHandlers; @@ -111,20 +112,25 @@ export type TypeNormalisedInput = { refreshTokenPath: NormalisedURLPath; accessTokenPath: NormalisedURLPath; cookieDomain: string | undefined; - getCookieSameSite: (input: { request: BaseRequest | undefined; userContext: any }) => "strict" | "lax" | "none"; + getCookieSameSite: (input: { + request: BaseRequest | undefined; + userContext: UserContext; + }) => "strict" | "lax" | "none"; cookieSecure: boolean; sessionExpiredStatusCode: number; errorHandlers: NormalisedErrorHandlers; + overwriteSessionDuringSignIn: boolean; + antiCsrfFunctionOrString: | "VIA_TOKEN" | "VIA_CUSTOM_HEADER" | "NONE" - | ((input: { request: BaseRequest | undefined; userContext: any }) => "VIA_CUSTOM_HEADER" | "NONE"); + | ((input: { request: BaseRequest | undefined; userContext: UserContext }) => "VIA_CUSTOM_HEADER" | "NONE"); getTokenTransferMethod: (input: { req: BaseRequest; forCreateNewSession: boolean; - userContext: any; + userContext: UserContext; }) => TokenTransferMethod | "any"; invalidClaimStatusCode: number; @@ -163,7 +169,7 @@ export interface SessionRequest extends BaseRequest { } export interface ErrorHandlerMiddleware { - (message: string, request: BaseRequest, response: BaseResponse): Promise; + (message: string, request: BaseRequest, response: BaseResponse, userContext: UserContext): Promise; } export interface TokenTheftErrorHandlerMiddleware { @@ -172,12 +178,18 @@ export interface TokenTheftErrorHandlerMiddleware { userId: string, recipeUserId: RecipeUserId, request: BaseRequest, - response: BaseResponse + response: BaseResponse, + userContext: UserContext ): Promise; } export interface InvalidClaimErrorHandlerMiddleware { - (validatorErrors: ClaimValidationError[], request: BaseRequest, response: BaseResponse): Promise; + ( + validatorErrors: ClaimValidationError[], + request: BaseRequest, + response: BaseResponse, + userContext: UserContext + ): Promise; } export interface NormalisedErrorHandlers { @@ -194,7 +206,7 @@ export interface VerifySessionOptions { overrideGlobalClaimValidators?: ( globalClaimValidators: SessionClaimValidator[], session: SessionContainerInterface, - userContext: any + userContext: UserContext ) => Promise | SessionClaimValidator[]; } @@ -206,7 +218,7 @@ export type RecipeInterface = { sessionDataInDatabase?: any; disableAntiCsrf?: boolean; tenantId: string; - userContext: any; + userContext: UserContext; }): Promise; getGlobalClaimValidators(input: { @@ -214,21 +226,21 @@ export type RecipeInterface = { userId: string; recipeUserId: RecipeUserId; claimValidatorsAddedByOtherRecipes: SessionClaimValidator[]; - userContext: any; + userContext: UserContext; }): Promise | SessionClaimValidator[]; getSession(input: { accessToken: string | undefined; antiCsrfToken?: string; options?: VerifySessionOptions; - userContext: any; + userContext: UserContext; }): Promise; refreshSession(input: { refreshToken: string; antiCsrfToken?: string; disableAntiCsrf: boolean; - userContext: any; + userContext: UserContext; }): Promise; /** @@ -238,14 +250,17 @@ export type RecipeInterface = { * * Returns undefined if the sessionHandle does not exist */ - getSessionInformation(input: { sessionHandle: string; userContext: any }): Promise; + getSessionInformation(input: { + sessionHandle: string; + userContext: UserContext; + }): Promise; revokeAllSessionsForUser(input: { userId: string; revokeSessionsForLinkedAccounts: boolean; tenantId: string; revokeAcrossAllTenants?: boolean; - userContext: any; + userContext: UserContext; }): Promise; getAllSessionHandlesForUser(input: { @@ -253,24 +268,24 @@ export type RecipeInterface = { fetchSessionsForAllLinkedAccounts: boolean; tenantId: string; fetchAcrossAllTenants?: boolean; - userContext: any; + userContext: UserContext; }): Promise; - revokeSession(input: { sessionHandle: string; userContext: any }): Promise; + revokeSession(input: { sessionHandle: string; userContext: UserContext }): Promise; - revokeMultipleSessions(input: { sessionHandles: string[]; userContext: any }): Promise; + revokeMultipleSessions(input: { sessionHandles: string[]; userContext: UserContext }): Promise; // Returns false if the sessionHandle does not exist updateSessionDataInDatabase(input: { sessionHandle: string; newSessionData: any; - userContext: any; + userContext: UserContext; }): Promise; mergeIntoAccessTokenPayload(input: { sessionHandle: string; accessTokenPayloadUpdate: JSONObject; - userContext: any; + userContext: UserContext; }): Promise; /** @@ -279,7 +294,7 @@ export type RecipeInterface = { regenerateAccessToken(input: { accessToken: string; newAccessTokenPayload?: any; - userContext: any; + userContext: UserContext; }): Promise< | { status: "OK"; @@ -304,23 +319,27 @@ export type RecipeInterface = { recipeUserId: RecipeUserId; accessTokenPayload: any; claimValidators: SessionClaimValidator[]; - userContext: any; + userContext: UserContext; }): Promise<{ invalidClaims: ClaimValidationError[]; accessTokenPayloadUpdate?: any; }>; - fetchAndSetClaim(input: { sessionHandle: string; claim: SessionClaim; userContext: any }): Promise; + fetchAndSetClaim(input: { + sessionHandle: string; + claim: SessionClaim; + userContext: UserContext; + }): Promise; setClaimValue(input: { sessionHandle: string; claim: SessionClaim; value: T; - userContext: any; + userContext: UserContext; }): Promise; getClaimValue(input: { sessionHandle: string; claim: SessionClaim; - userContext: any; + userContext: UserContext; }): Promise< | { status: "SESSION_DOES_NOT_EXIST_ERROR"; @@ -331,24 +350,24 @@ export type RecipeInterface = { } >; - removeClaim(input: { sessionHandle: string; claim: SessionClaim; userContext: any }): Promise; + removeClaim(input: { sessionHandle: string; claim: SessionClaim; userContext: UserContext }): Promise; }; export interface SessionContainerInterface { - revokeSession(userContext?: any): Promise; + revokeSession(userContext?: UserContext): Promise; - getSessionDataFromDatabase(userContext?: any): Promise; + getSessionDataFromDatabase(userContext?: UserContext): Promise; - updateSessionDataInDatabase(newSessionData: any, userContext?: any): Promise; + updateSessionDataInDatabase(newSessionData: any, userContext?: UserContext): Promise; - getUserId(userContext?: any): string; + getUserId(userContext?: UserContext): string; - getRecipeUserId(userContext?: any): RecipeUserId; - getTenantId(userContext?: any): string; + getRecipeUserId(userContext?: UserContext): RecipeUserId; + getTenantId(userContext?: UserContext): string; - getAccessTokenPayload(userContext?: any): any; + getAccessTokenPayload(userContext?: UserContext): any; - getHandle(userContext?: any): string; + getHandle(userContext?: UserContext): string; getAllSessionTokensDangerously(): { accessToken: string; @@ -358,20 +377,20 @@ export interface SessionContainerInterface { accessAndFrontTokenUpdated: boolean; }; - getAccessToken(userContext?: any): string; + getAccessToken(userContext?: UserContext): string; - mergeIntoAccessTokenPayload(accessTokenPayloadUpdate: JSONObject, userContext?: any): Promise; + mergeIntoAccessTokenPayload(accessTokenPayloadUpdate: JSONObject, userContext?: UserContext): Promise; - getTimeCreated(userContext?: any): Promise; + getTimeCreated(userContext?: UserContext): Promise; - getExpiry(userContext?: any): Promise; + getExpiry(userContext?: UserContext): Promise; - assertClaims(claimValidators: SessionClaimValidator[], userContext?: any): Promise; - fetchAndSetClaim(claim: SessionClaim, userContext?: any): Promise; - setClaimValue(claim: SessionClaim, value: T, userContext?: any): Promise; - getClaimValue(claim: SessionClaim, userContext?: any): Promise; - removeClaim(claim: SessionClaim, userContext?: any): Promise; - attachToRequestResponse(reqResInfo: ReqResInfo, userContext?: any): Promise | void; + assertClaims(claimValidators: SessionClaimValidator[], userContext?: UserContext): Promise; + fetchAndSetClaim(claim: SessionClaim, userContext?: UserContext): Promise; + setClaimValue(claim: SessionClaim, value: T, userContext?: UserContext): Promise; + getClaimValue(claim: SessionClaim, userContext?: UserContext): Promise; + removeClaim(claim: SessionClaim, userContext?: UserContext): Promise; + attachToRequestResponse(reqResInfo: ReqResInfo, userContext?: UserContext): Promise | void; } export type APIOptions = { @@ -389,7 +408,9 @@ export type APIInterface = { * since it's not something that is directly called by the user on the * frontend anyway */ - refreshPOST: undefined | ((input: { options: APIOptions; userContext: any }) => Promise); + refreshPOST: + | undefined + | ((input: { options: APIOptions; userContext: UserContext }) => Promise); signOutPOST: | undefined @@ -400,7 +421,7 @@ export type APIInterface = { // rare that something needs to be done in this case, but making it like this // has little disadvantages. session: SessionContainerInterface | undefined; - userContext: any; + userContext: UserContext; }) => Promise< | { status: "OK"; @@ -411,7 +432,7 @@ export type APIInterface = { verifySession(input: { verifySessionOptions: VerifySessionOptions | undefined; options: APIOptions; - userContext: any; + userContext: UserContext; }): Promise; }; @@ -440,7 +461,7 @@ export type SessionClaimValidator = ( * Decides if we need to refetch the claim value before checking the payload with `isValid`. * E.g.: if the information in the payload is expired, or is not sufficient for this check. */ - shouldRefetch: (payload: any, userContext: any) => Promise | boolean; + shouldRefetch: (payload: any, userContext: UserContext) => Promise | boolean; } | {} ) & { @@ -448,7 +469,7 @@ export type SessionClaimValidator = ( /** * Decides if the claim is valid based on the payload (and not checking DB or anything else) */ - validate: (payload: any, userContext: any) => Promise; + validate: (payload: any, userContext: UserContext) => Promise; }; export abstract class SessionClaim { @@ -463,7 +484,8 @@ export abstract class SessionClaim { userId: string, recipeUserId: RecipeUserId, tenantId: string, - userContext: any + currentPayload: JSONObject | undefined, + userContext: UserContext ): Promise | T | undefined; /** @@ -471,31 +493,37 @@ export abstract class SessionClaim { * * @returns The modified payload object */ - abstract addToPayload_internal(payload: JSONObject, value: T, userContext: any): JSONObject; + abstract addToPayload_internal(payload: JSONObject, value: T, userContext: UserContext): JSONObject; /** * Removes the claim from the payload by setting it to null, so mergeIntoAccessTokenPayload clears it * * @returns The modified payload object */ - abstract removeFromPayloadByMerge_internal(payload: JSONObject, userContext?: any): JSONObject; + abstract removeFromPayloadByMerge_internal(payload: JSONObject, userContext?: UserContext): JSONObject; /** * Removes the claim from the payload, by cloning and updating the entire object. * * @returns The modified payload object */ - abstract removeFromPayload(payload: JSONObject, userContext?: any): JSONObject; + abstract removeFromPayload(payload: JSONObject, userContext?: UserContext): JSONObject; /** * Gets the value of the claim stored in the payload * * @returns Claim value */ - abstract getValueFromPayload(payload: JSONObject, userContext: any): T | undefined; + abstract getValueFromPayload(payload: JSONObject, userContext: UserContext): T | undefined; - async build(userId: string, recipeUserId: RecipeUserId, tenantId: string, userContext?: any): Promise { - const value = await this.fetchValue(userId, recipeUserId, tenantId, userContext); + async build( + userId: string, + recipeUserId: RecipeUserId, + tenantId: string, + currentPayload: JSONObject | undefined, + userContext: UserContext + ): Promise { + const value = await this.fetchValue(userId, recipeUserId, tenantId, currentPayload, userContext); if (value === undefined) { return {}; diff --git a/lib/ts/recipe/session/utils.ts b/lib/ts/recipe/session/utils.ts index 2e73b4f42..b40c9608c 100644 --- a/lib/ts/recipe/session/utils.ts +++ b/lib/ts/recipe/session/utils.ts @@ -27,7 +27,7 @@ import { setFrontTokenInHeaders, setToken, getAuthModeFromHeader } from "./cooki import SessionRecipe from "./recipe"; import { REFRESH_API_PATH, hundredYearsInMs } from "./constants"; import NormalisedURLPath from "../../normalisedURLPath"; -import { NormalisedAppinfo } from "../../types"; +import { NormalisedAppinfo, UserContext } from "../../types"; import { isAnIpAddress } from "../../utils"; import { RecipeInterface, APIInterface } from "./types"; import type { BaseRequest, BaseResponse } from "../../framework"; @@ -39,7 +39,8 @@ export async function sendTryRefreshTokenResponse( recipeInstance: SessionRecipe, _: string, __: BaseRequest, - response: BaseResponse + response: BaseResponse, + ___: UserContext ) { sendNon200ResponseWithMessage(response, "try refresh token", recipeInstance.config.sessionExpiredStatusCode); } @@ -48,7 +49,8 @@ export async function sendUnauthorisedResponse( recipeInstance: SessionRecipe, _: string, __: BaseRequest, - response: BaseResponse + response: BaseResponse, + ___: UserContext ) { sendNon200ResponseWithMessage(response, "unauthorised", recipeInstance.config.sessionExpiredStatusCode); } @@ -57,7 +59,8 @@ export async function sendInvalidClaimResponse( recipeInstance: SessionRecipe, claimValidationErrors: ClaimValidationError[], __: BaseRequest, - response: BaseResponse + response: BaseResponse, + ___: UserContext ) { sendNon200Response(response, recipeInstance.config.invalidClaimStatusCode, { message: "invalid claim", @@ -71,9 +74,10 @@ export async function sendTokenTheftDetectedResponse( _: string, __: RecipeUserId, ___: BaseRequest, - response: BaseResponse + response: BaseResponse, + userContext: UserContext ) { - await recipeInstance.recipeInterfaceImpl.revokeSession({ sessionHandle, userContext: {} }); + await recipeInstance.recipeInterfaceImpl.revokeSession({ sessionHandle, userContext }); sendNon200ResponseWithMessage(response, "token theft detected", recipeInstance.config.sessionExpiredStatusCode); } @@ -140,8 +144,8 @@ export function validateAndNormaliseUserInput( let cookieSameSite: (input: { request: BaseRequest | undefined; - userContext: any; - }) => "strict" | "lax" | "none" = (input: { request: BaseRequest | undefined; userContext: any }) => { + userContext: UserContext; + }) => "strict" | "lax" | "none" = (input: { request: BaseRequest | undefined; userContext: UserContext }) => { let protocolOfWebsiteDomain = getURLProtocol( appInfo .getOrigin({ @@ -186,7 +190,7 @@ export function validateAndNormaliseUserInput( | "VIA_TOKEN" | "VIA_CUSTOM_HEADER" | "NONE" - | ((input: { request: BaseRequest | undefined; userContext: any }) => "VIA_CUSTOM_HEADER" | "NONE") = ({ + | ((input: { request: BaseRequest | undefined; userContext: UserContext }) => "VIA_CUSTOM_HEADER" | "NONE") = ({ request, userContext, }) => { @@ -212,7 +216,8 @@ export function validateAndNormaliseUserInput( userId: string, recipeUserId: RecipeUserId, request: BaseRequest, - response: BaseResponse + response: BaseResponse, + userContext: UserContext ) => { return await sendTokenTheftDetectedResponse( recipeInstance, @@ -220,17 +225,33 @@ export function validateAndNormaliseUserInput( userId, recipeUserId, request, - response + response, + userContext ); }, - onTryRefreshToken: async (message: string, request: BaseRequest, response: BaseResponse) => { - return await sendTryRefreshTokenResponse(recipeInstance, message, request, response); + onTryRefreshToken: async ( + message: string, + request: BaseRequest, + response: BaseResponse, + userContext: UserContext + ) => { + return await sendTryRefreshTokenResponse(recipeInstance, message, request, response, userContext); }, - onUnauthorised: async (message: string, request: BaseRequest, response: BaseResponse) => { - return await sendUnauthorisedResponse(recipeInstance, message, request, response); + onUnauthorised: async ( + message: string, + request: BaseRequest, + response: BaseResponse, + userContext: UserContext + ) => { + return await sendUnauthorisedResponse(recipeInstance, message, request, response, userContext); }, - onInvalidClaim: (validationErrors: ClaimValidationError[], request: BaseRequest, response: BaseResponse) => { - return sendInvalidClaimResponse(recipeInstance, validationErrors, request, response); + onInvalidClaim: ( + validationErrors: ClaimValidationError[], + request: BaseRequest, + response: BaseResponse, + userContext: UserContext + ) => { + return sendInvalidClaimResponse(recipeInstance, validationErrors, request, response, userContext); }, }; if (config !== undefined && config.errorHandlers !== undefined) { @@ -268,6 +289,7 @@ export function validateAndNormaliseUserInput( antiCsrfFunctionOrString: antiCsrf, override, invalidClaimStatusCode, + overwriteSessionDuringSignIn: config?.overwriteSessionDuringSignIn ?? false, }; } @@ -287,7 +309,7 @@ export function setAccessTokenInResponse( config: TypeNormalisedInput, transferMethod: TokenTransferMethod, req: BaseRequest | undefined, - userContext: any + userContext: UserContext ) { setFrontTokenInHeaders(res, frontToken); setToken( @@ -326,7 +348,7 @@ export function setAccessTokenInResponse( export async function getRequiredClaimValidators( session: SessionContainerInterface, overrideGlobalClaimValidators: VerifySessionOptions["overrideGlobalClaimValidators"], - userContext: any + userContext: UserContext ) { const claimValidatorsAddedByOtherRecipes = SessionRecipe.getInstanceOrThrowError().getClaimValidatorsAddedByOtherRecipes(); const globalClaimValidators: SessionClaimValidator[] = await SessionRecipe.getInstanceOrThrowError().recipeInterfaceImpl.getGlobalClaimValidators( @@ -347,7 +369,7 @@ export async function getRequiredClaimValidators( export async function validateClaimsInPayload( claimValidators: SessionClaimValidator[], newAccessTokenPayload: any, - userContext: any + userContext: UserContext ) { const validationErrors = []; for (const validator of claimValidators) { diff --git a/lib/ts/recipe/thirdparty/api/appleRedirect.ts b/lib/ts/recipe/thirdparty/api/appleRedirect.ts index b7e34d6b9..3e1e3c1ac 100644 --- a/lib/ts/recipe/thirdparty/api/appleRedirect.ts +++ b/lib/ts/recipe/thirdparty/api/appleRedirect.ts @@ -14,11 +14,12 @@ */ import { APIInterface, APIOptions } from "../"; +import { UserContext } from "../../../types"; export default async function appleRedirectHandler( apiImplementation: APIInterface, options: APIOptions, - userContext: any + userContext: UserContext ): Promise { if (apiImplementation.appleRedirectHandlerPOST === undefined) { return false; diff --git a/lib/ts/recipe/thirdparty/api/authorisationUrl.ts b/lib/ts/recipe/thirdparty/api/authorisationUrl.ts index afb0600e8..21ad28366 100644 --- a/lib/ts/recipe/thirdparty/api/authorisationUrl.ts +++ b/lib/ts/recipe/thirdparty/api/authorisationUrl.ts @@ -16,12 +16,13 @@ import { send200Response } from "../../../utils"; import STError from "../error"; import { APIInterface, APIOptions } from "../"; +import { UserContext } from "../../../types"; export default async function authorisationUrlAPI( apiImplementation: APIInterface, tenantId: string, options: APIOptions, - userContext: any + userContext: UserContext ): Promise { if (apiImplementation.authorisationUrlGET === undefined) { return false; diff --git a/lib/ts/recipe/thirdparty/api/implementation.ts b/lib/ts/recipe/thirdparty/api/implementation.ts index 08a291b36..fc9f64125 100644 --- a/lib/ts/recipe/thirdparty/api/implementation.ts +++ b/lib/ts/recipe/thirdparty/api/implementation.ts @@ -3,10 +3,14 @@ import Session from "../../session"; import AccountLinking from "../../accountlinking/recipe"; import { RecipeLevelUser } from "../../accountlinking/types"; -import { listUsersByAccountInfo } from "../../.."; +import { getUser, listUsersByAccountInfo } from "../../.."; import RecipeUserId from "../../../recipeUserId"; import EmailVerification from "../../emailverification"; import EmailVerificationRecipe from "../../emailverification/recipe"; +import MultiFactorAuthRecipe from "../../multifactorauth/recipe"; +import { SessionContainerInterface } from "../../session/types"; +import { User } from "../../../types"; +import SessionError from "../../session/error"; export default function getAPIInterface(): APIInterface { return { @@ -198,6 +202,54 @@ export default function getAPIInterface(): APIInterface { } } + const userLoggingIn = existingUsers[0]; + + const mfaInstance = MultiFactorAuthRecipe.getInstance(); + + let session: SessionContainerInterface | undefined = await Session.getSession( + input.options.req, + input.options.res, + { sessionRequired: false, overrideGlobalClaimValidators: () => [] } + ); + let sessionUser: User | undefined; + if (session !== undefined) { + if (userLoggingIn && userLoggingIn.id === session.getUserId()) { + sessionUser = userLoggingIn; + } else { + const user = await getUser(session.getUserId(), input.userContext); + if (user === undefined) { + throw new SessionError({ + type: SessionError.UNAUTHORISED, + message: "Session user not found", + }); + } + sessionUser = user; + } + } + + let isAlreadySetup = undefined; + if (mfaInstance) { + isAlreadySetup = !sessionUser ? false : sessionUser.thirdParty.length > 0; + const validateMfaRes = await mfaInstance.validateForMultifactorAuthBeforeFactorCompletion({ + req: input.options.req, + res: input.options.res, + tenantId: input.tenantId, + factorIdInProgress: "thirdparty", + session, + userLoggingIn, + isAlreadySetup, + signUpInfo: { + email: emailInfo.id, + isVerifiedFactor: emailInfo.isVerified, + }, + userContext: input.userContext, + }); + + if (validateMfaRes.status !== "OK") { + return validateMfaRes; + } + } + let response = await options.recipeImplementation.signInUp({ thirdPartyId: provider.id, thirdPartyUserId: userInfo.thirdPartyUserId, @@ -207,6 +259,9 @@ export default function getAPIInterface(): APIInterface { oAuthTokens: oAuthTokensToUse, rawUserInfoFromProvider: userInfo.rawUserInfoFromProvider, tenantId, + + // we do not want to attempt accountlinking when there is an active session and MFA is turned on + shouldAttemptAccountLinkingIfAllowed: session === undefined || mfaInstance === undefined, userContext, }); @@ -257,28 +312,59 @@ export default function getAPIInterface(): APIInterface { // we do account linking only during sign in here cause during sign up, // the recipe function above does account linking for us. - response.user = await AccountLinking.getInstance().createPrimaryUserIdOrLinkAccounts({ + // we do not want to attempt accountlinking when there is an active session and MFA is turned on + if (session === undefined || mfaInstance === undefined) { + response.user = await AccountLinking.getInstance().createPrimaryUserIdOrLinkAccounts({ + tenantId, + user: response.user, + userContext, + }); + } + } + + if (mfaInstance === undefined) { + // No MFA, create session as usual + let session = await Session.createNewOrKeepExistingSession( + options.req, + options.res, tenantId, + loginMethod.recipeUserId, + {}, + {}, + userContext + ); + return { + status: "OK", + createdNewRecipeUser: response.createdNewRecipeUser, user: response.user, - userContext, - }); + session, + oAuthTokens: oAuthTokensToUse, + rawUserInfoFromProvider: userInfo.rawUserInfoFromProvider, + }; } - let session = await Session.createNewSession( - options.req, - options.res, + const sessionRes = await mfaInstance.createOrUpdateSessionForMultifactorAuthAfterFactorCompletion({ + req: options.req, + res: options.res, tenantId, - loginMethod.recipeUserId, - {}, - {}, - userContext - ); + factorIdInProgress: "thirdparty", + isAlreadySetup, + justCompletedFactorUserInfo: { + user: response.user, + createdNewUser: response.createdNewRecipeUser, + recipeUserId: loginMethod.recipeUserId, + }, + userContext: input.userContext, + }); + if (sessionRes.status !== "OK") { + return sessionRes; + } return { status: "OK", createdNewRecipeUser: response.createdNewRecipeUser, - user: response.user, - session, + user: (await getUser(response.user.id, userContext))!, // fetching user again cause the user might have been updated while setting up mfa + session: sessionRes.session, oAuthTokens: oAuthTokensToUse, rawUserInfoFromProvider: userInfo.rawUserInfoFromProvider, }; diff --git a/lib/ts/recipe/thirdparty/api/signinup.ts b/lib/ts/recipe/thirdparty/api/signinup.ts index 698f5dda0..d31326654 100644 --- a/lib/ts/recipe/thirdparty/api/signinup.ts +++ b/lib/ts/recipe/thirdparty/api/signinup.ts @@ -16,12 +16,13 @@ import STError from "../error"; import { getBackwardsCompatibleUserInfo, send200Response } from "../../../utils"; import { APIInterface, APIOptions } from "../"; +import { UserContext } from "../../../types"; export default async function signInUpAPI( apiImplementation: APIInterface, tenantId: string, options: APIOptions, - userContext: any + userContext: UserContext ): Promise { if (apiImplementation.signInUpPOST === undefined) { return false; diff --git a/lib/ts/recipe/thirdparty/index.ts b/lib/ts/recipe/thirdparty/index.ts index ff104b08e..b93e2a5aa 100644 --- a/lib/ts/recipe/thirdparty/index.ts +++ b/lib/ts/recipe/thirdparty/index.ts @@ -17,6 +17,7 @@ import Recipe from "./recipe"; import SuperTokensError from "./error"; import { RecipeInterface, APIInterface, APIOptions, TypeProvider } from "./types"; import { DEFAULT_TENANT_ID } from "../multitenancy/constants"; +import { getUserContext } from "../../utils"; export default class Wrapper { static init = Recipe.init; @@ -27,13 +28,13 @@ export default class Wrapper { tenantId: string, thirdPartyId: string, clientType: string | undefined, - userContext: any = {} + userContext?: Record ) { return await Recipe.getInstanceOrThrowError().recipeInterfaceImpl.getProvider({ thirdPartyId, clientType, tenantId, - userContext, + userContext: getUserContext(userContext), }); } @@ -43,7 +44,8 @@ export default class Wrapper { thirdPartyUserId: string, email: string, isVerified: boolean, - userContext: any = {} + shouldAttemptAccountLinkingIfAllowed?: boolean, + userContext?: Record ) { return await Recipe.getInstanceOrThrowError().recipeInterfaceImpl.manuallyCreateOrUpdateUser({ thirdPartyId, @@ -51,7 +53,8 @@ export default class Wrapper { email, tenantId: tenantId === undefined ? DEFAULT_TENANT_ID : tenantId, isVerified, - userContext, + shouldAttemptAccountLinkingIfAllowed, + userContext: getUserContext(userContext), }); } } diff --git a/lib/ts/recipe/thirdparty/providers/configUtils.ts b/lib/ts/recipe/thirdparty/providers/configUtils.ts index ea4d31ad6..01eadcd34 100644 --- a/lib/ts/recipe/thirdparty/providers/configUtils.ts +++ b/lib/ts/recipe/thirdparty/providers/configUtils.ts @@ -13,6 +13,7 @@ import { Okta, Twitter, } from "."; +import { UserContext } from "../../../types"; import { ProviderClientConfig, ProviderConfig, @@ -33,7 +34,7 @@ export function getProviderConfigForClient( }; } -async function fetchAndSetConfig(provider: TypeProvider, clientType: string | undefined, userContext: any) { +async function fetchAndSetConfig(provider: TypeProvider, clientType: string | undefined, userContext: UserContext) { let config = await provider.getConfigForClientType({ clientType, userContext }); config = await discoverOIDCEndpoints(config); @@ -77,7 +78,7 @@ export async function findAndCreateProviderInstance( providers: ProviderInput[], thirdPartyId: string, clientType: string | undefined, - userContext: any + userContext: UserContext ): Promise { for (const providerInput of providers) { if (providerInput.config.thirdPartyId === thirdPartyId) { diff --git a/lib/ts/recipe/thirdparty/recipe.ts b/lib/ts/recipe/thirdparty/recipe.ts index 8d57780a4..ea89bd9ef 100644 --- a/lib/ts/recipe/thirdparty/recipe.ts +++ b/lib/ts/recipe/thirdparty/recipe.ts @@ -14,7 +14,7 @@ */ import RecipeModule from "../../recipeModule"; -import { NormalisedAppinfo, APIHandled, RecipeListFunction, HTTPMethod } from "../../types"; +import { NormalisedAppinfo, APIHandled, RecipeListFunction, HTTPMethod, UserContext } from "../../types"; import { TypeInput, TypeNormalisedInput, RecipeInterface, APIInterface, ProviderInput } from "./types"; import { validateAndNormaliseUserInput } from "./utils"; import MultitenancyRecipe from "../multitenancy/recipe"; @@ -31,6 +31,9 @@ import type { BaseRequest, BaseResponse } from "../../framework"; import appleRedirectHandler from "./api/appleRedirect"; import OverrideableBuilder from "supertokens-js-override"; import { PostSuperTokensInitCallbacks } from "../../postSuperTokensInitCallbacks"; +import MultiFactorAuthRecipe from "../multifactorauth/recipe"; +import { User } from "../../user"; +import { TenantConfig } from "../multitenancy/types"; export default class Recipe extends RecipeModule { private static instance: Recipe | undefined = undefined; @@ -92,6 +95,38 @@ export default class Recipe extends RecipeModule { emailDelivery: undefined, } ); + + PostSuperTokensInitCallbacks.addPostInitCallback(() => { + const mfaInstance = MultiFactorAuthRecipe.getInstance(); + if (mfaInstance !== undefined) { + mfaInstance.addGetAllFactorsFromOtherRecipesFunc((tenantConfig) => { + if (tenantConfig.thirdParty.enabled === false) { + return { + factorIds: [], + firstFactorIds: [], + }; + } + return { + factorIds: ["thirdparty"], + firstFactorIds: ["thirdparty"], + }; + }); + mfaInstance.addGetFactorsSetupForUserFromOtherRecipes( + async (user: User, tenantConfig: TenantConfig) => { + if (tenantConfig.thirdParty.enabled === false) { + return []; + } + for (const loginMethod of user.loginMethods) { + if (loginMethod.recipeId === Recipe.RECIPE_ID) { + return ["thirdparty"]; + } + } + return []; + } + ); + } + }); + return Recipe.instance; } else { throw new Error("ThirdParty recipe has already been initialised. Please check your code for bugs."); @@ -143,7 +178,7 @@ export default class Recipe extends RecipeModule { res: BaseResponse, _path: NormalisedURLPath, _method: HTTPMethod, - userContext: any + userContext: UserContext ): Promise => { let options = { config: this.config, diff --git a/lib/ts/recipe/thirdparty/recipeImplementation.ts b/lib/ts/recipe/thirdparty/recipeImplementation.ts index ee2bd5725..59065c4e5 100644 --- a/lib/ts/recipe/thirdparty/recipeImplementation.ts +++ b/lib/ts/recipe/thirdparty/recipeImplementation.ts @@ -6,7 +6,7 @@ import AccountLinking from "../accountlinking/recipe"; import MultitenancyRecipe from "../multitenancy/recipe"; import RecipeUserId from "../../recipeUserId"; import { getUser } from "../.."; -import { User as UserType } from "../../types"; +import { UserContext, User as UserType } from "../../types"; import { User } from "../../user"; export default function getRecipeImplementation(querier: Querier, providers: ProviderInput[]): RecipeInterface { @@ -19,6 +19,7 @@ export default function getRecipeImplementation(querier: Querier, providers: Pro email, isVerified, tenantId, + shouldAttemptAccountLinkingIfAllowed, userContext, }: { thirdPartyId: string; @@ -26,7 +27,8 @@ export default function getRecipeImplementation(querier: Querier, providers: Pro email: string; isVerified: boolean; tenantId: string; - userContext: any; + shouldAttemptAccountLinkingIfAllowed?: boolean; + userContext: UserContext; } ): Promise< | { @@ -34,7 +36,6 @@ export default function getRecipeImplementation(querier: Querier, providers: Pro createdNewRecipeUser: boolean; user: UserType; recipeUserId: RecipeUserId; - isValidFirstFactorForTenant: boolean | undefined; } | { status: "EMAIL_CHANGE_NOT_ALLOWED_ERROR"; @@ -62,11 +63,13 @@ export default function getRecipeImplementation(querier: Querier, providers: Pro response.user = new User(response.user); response.recipeUserId = new RecipeUserId(response.recipeUserId); - await AccountLinking.getInstance().verifyEmailForRecipeUserIfLinkedAccountsAreVerified({ - user: response.user, - recipeUserId: response.recipeUserId, - userContext, - }); + if (shouldAttemptAccountLinkingIfAllowed ?? true) { + await AccountLinking.getInstance().verifyEmailForRecipeUserIfLinkedAccountsAreVerified({ + user: response.user, + recipeUserId: response.recipeUserId, + userContext, + }); + } // we do this so that we get the updated user (in case the above // function updated the verification status) and can return that @@ -87,7 +90,6 @@ export default function getRecipeImplementation(querier: Querier, providers: Pro createdNewRecipeUser: response.createdNewUser, user: response.user, recipeUserId: response.recipeUserId, - isValidFirstFactorForTenant: response.isValidFirstFactorForTenant, }; } @@ -102,7 +104,6 @@ export default function getRecipeImplementation(querier: Querier, providers: Pro createdNewRecipeUser: response.createdNewUser, user: updatedUser, recipeUserId: response.recipeUserId, - isValidFirstFactorForTenant: response.isValidFirstFactorForTenant, }; }, @@ -117,18 +118,20 @@ export default function getRecipeImplementation(querier: Querier, providers: Pro userContext, oAuthTokens, rawUserInfoFromProvider, + shouldAttemptAccountLinkingIfAllowed, }: { thirdPartyId: string; thirdPartyUserId: string; email: string; isVerified: boolean; tenantId: string; - userContext: any; + userContext: UserContext; oAuthTokens: { [key: string]: any }; rawUserInfoFromProvider: { fromIdTokenPayload?: { [key: string]: any }; fromUserInfoAPI?: { [key: string]: any }; }; + shouldAttemptAccountLinkingIfAllowed?: boolean; } ): Promise< | { @@ -141,7 +144,6 @@ export default function getRecipeImplementation(querier: Querier, providers: Pro fromIdTokenPayload?: { [key: string]: any }; fromUserInfoAPI?: { [key: string]: any }; }; - isValidFirstFactorForTenant: boolean | undefined; } | { status: "SIGN_IN_UP_NOT_ALLOWED"; @@ -154,6 +156,7 @@ export default function getRecipeImplementation(querier: Querier, providers: Pro email, tenantId, isVerified, + shouldAttemptAccountLinkingIfAllowed, userContext, }); diff --git a/lib/ts/recipe/thirdparty/types.ts b/lib/ts/recipe/thirdparty/types.ts index f093a91dc..19ad5ccfc 100644 --- a/lib/ts/recipe/thirdparty/types.ts +++ b/lib/ts/recipe/thirdparty/types.ts @@ -14,11 +14,12 @@ */ import type { BaseRequest, BaseResponse } from "../../framework"; -import { NormalisedAppinfo } from "../../types"; +import { NormalisedAppinfo, UserContext } from "../../types"; import OverrideableBuilder from "supertokens-js-override"; import { SessionContainerInterface } from "../session/types"; import { GeneralErrorResponse, User } from "../../types"; import RecipeUserId from "../../recipeUserId"; +import { MFAFlowErrors } from "../multifactorauth/types"; export type UserInfo = { thirdPartyUserId: string; @@ -65,7 +66,7 @@ type CommonProviderConfig = { validateIdTokenPayload?: (input: { idTokenPayload: { [key: string]: any }; clientConfig: ProviderConfigForClientType; - userContext: any; + userContext: UserContext; }) => Promise; /** * This function is responsible for validating the access token received from the third party provider. @@ -80,10 +81,14 @@ type CommonProviderConfig = { validateAccessToken?: (input: { accessToken: string; clientConfig: ProviderConfigForClientType; - userContext: any; + userContext: UserContext; }) => Promise; requireEmail?: boolean; - generateFakeEmail?: (input: { thirdPartyUserId: string; tenantId: string; userContext: any }) => Promise; + generateFakeEmail?: (input: { + thirdPartyUserId: string; + tenantId: string; + userContext: UserContext; + }) => Promise; }; export type ProviderConfigForClientType = ProviderClientConfig & CommonProviderConfig; @@ -92,10 +97,13 @@ export type TypeProvider = { id: string; config: ProviderConfigForClientType; - getConfigForClientType: (input: { clientType?: string; userContext: any }) => Promise; + getConfigForClientType: (input: { + clientType?: string; + userContext: UserContext; + }) => Promise; getAuthorisationRedirectURL: (input: { redirectURIOnProviderDashboard: string; - userContext: any; + userContext: UserContext; }) => Promise<{ urlWithQueryParams: string; pkceCodeVerifier?: string }>; exchangeAuthCodeForOAuthTokens: (input: { redirectURIInfo: { @@ -103,9 +111,9 @@ export type TypeProvider = { redirectURIQueryParams: any; pkceCodeVerifier?: string; }; - userContext: any; + userContext: UserContext; }) => Promise; - getUserInfo: (input: { oAuthTokens: any; userContext: any }) => Promise; + getUserInfo: (input: { oAuthTokens: any; userContext: UserContext }) => Promise; }; export type ProviderConfig = CommonProviderConfig & { @@ -152,7 +160,7 @@ export type RecipeInterface = { thirdPartyId: string; tenantId: string; clientType?: string; - userContext: any; + userContext: UserContext; }): Promise; signInUp(input: { @@ -166,7 +174,8 @@ export type RecipeInterface = { fromUserInfoAPI?: { [key: string]: any }; }; tenantId: string; - userContext: any; + shouldAttemptAccountLinkingIfAllowed?: boolean; + userContext: UserContext; }): Promise< | { status: "OK"; @@ -178,7 +187,6 @@ export type RecipeInterface = { fromIdTokenPayload?: { [key: string]: any }; fromUserInfoAPI?: { [key: string]: any }; }; - isValidFirstFactorForTenant: boolean | undefined; } | { status: "SIGN_IN_UP_NOT_ALLOWED"; @@ -192,14 +200,14 @@ export type RecipeInterface = { email: string; isVerified: boolean; tenantId: string; - userContext: any; + shouldAttemptAccountLinkingIfAllowed?: boolean; + userContext: UserContext; }): Promise< | { status: "OK"; createdNewRecipeUser: boolean; user: User; recipeUserId: RecipeUserId; - isValidFirstFactorForTenant: boolean | undefined; } | { status: "EMAIL_CHANGE_NOT_ALLOWED_ERROR"; @@ -231,7 +239,7 @@ export type APIInterface = { redirectURIOnProviderDashboard: string; tenantId: string; options: APIOptions; - userContext: any; + userContext: UserContext; }) => Promise< | { status: "OK"; @@ -248,7 +256,7 @@ export type APIInterface = { provider: TypeProvider; tenantId: string; options: APIOptions; - userContext: any; + userContext: UserContext; } & ( | { redirectURIInfo: { @@ -278,6 +286,7 @@ export type APIInterface = { status: "SIGN_IN_UP_NOT_ALLOWED"; reason: string; } + | MFAFlowErrors | GeneralErrorResponse >); @@ -286,6 +295,6 @@ export type APIInterface = { | ((input: { formPostInfoFromProvider: { [key: string]: any }; options: APIOptions; - userContext: any; + userContext: UserContext; }) => Promise); }; diff --git a/lib/ts/recipe/thirdpartyemailpassword/emaildelivery/services/backwardCompatibility/index.ts b/lib/ts/recipe/thirdpartyemailpassword/emaildelivery/services/backwardCompatibility/index.ts index bd04ddf2e..b520786b8 100644 --- a/lib/ts/recipe/thirdpartyemailpassword/emaildelivery/services/backwardCompatibility/index.ts +++ b/lib/ts/recipe/thirdpartyemailpassword/emaildelivery/services/backwardCompatibility/index.ts @@ -13,7 +13,7 @@ * under the License. */ import { TypeThirdPartyEmailPasswordEmailDeliveryInput } from "../../../types"; -import { NormalisedAppinfo } from "../../../../../types"; +import { NormalisedAppinfo, UserContext } from "../../../../../types"; import EmailPasswordBackwardCompatibilityService from "../../../../emailpassword/emaildelivery/services/backwardCompatibility"; import { EmailDeliveryInterface } from "../../../../../ingredients/emaildelivery/types"; @@ -30,7 +30,7 @@ export default class BackwardCompatibilityService } } - sendEmail = async (input: TypeThirdPartyEmailPasswordEmailDeliveryInput & { userContext: any }) => { + sendEmail = async (input: TypeThirdPartyEmailPasswordEmailDeliveryInput & { userContext: UserContext }) => { await this.emailPasswordBackwardCompatibilityService.sendEmail(input); }; } diff --git a/lib/ts/recipe/thirdpartyemailpassword/emaildelivery/services/smtp/index.ts b/lib/ts/recipe/thirdpartyemailpassword/emaildelivery/services/smtp/index.ts index 2534012c1..58d2158e9 100644 --- a/lib/ts/recipe/thirdpartyemailpassword/emaildelivery/services/smtp/index.ts +++ b/lib/ts/recipe/thirdpartyemailpassword/emaildelivery/services/smtp/index.ts @@ -16,6 +16,7 @@ import { TypeInput } from "../../../../../ingredients/emaildelivery/services/smt import { EmailDeliveryInterface } from "../../../../../ingredients/emaildelivery/types"; import { TypeThirdPartyEmailPasswordEmailDeliveryInput } from "../../../types"; import EmailPasswordSMTPService from "../../../../emailpassword/emaildelivery/services/smtp"; +import { UserContext } from "../../../../../types"; export default class SMTPService implements EmailDeliveryInterface { private emailPasswordSMTPService: EmailPasswordSMTPService; @@ -24,7 +25,7 @@ export default class SMTPService implements EmailDeliveryInterface { + sendEmail = async (input: TypeThirdPartyEmailPasswordEmailDeliveryInput & { userContext: UserContext }) => { await this.emailPasswordSMTPService.sendEmail(input); }; } diff --git a/lib/ts/recipe/thirdpartyemailpassword/index.ts b/lib/ts/recipe/thirdpartyemailpassword/index.ts index e9eb1b0b9..afcc6b224 100644 --- a/lib/ts/recipe/thirdpartyemailpassword/index.ts +++ b/lib/ts/recipe/thirdpartyemailpassword/index.ts @@ -22,6 +22,7 @@ import RecipeUserId from "../../recipeUserId"; import { DEFAULT_TENANT_ID } from "../multitenancy/constants"; import { getPasswordResetLink } from "../emailpassword/utils"; import { getRequestFromUserContext, getUser } from "../.."; +import { getUserContext } from "../../utils"; export default class Wrapper { static init = Recipe.init; @@ -32,13 +33,13 @@ export default class Wrapper { tenantId: string, thirdPartyId: string, clientType: string | undefined, - userContext: any = {} + userContext?: Record ) { return await Recipe.getInstanceOrThrowError().recipeInterfaceImpl.thirdPartyGetProvider({ thirdPartyId, clientType, tenantId, - userContext, + userContext: getUserContext(userContext), }); } @@ -48,7 +49,8 @@ export default class Wrapper { thirdPartyUserId: string, email: string, isVerified: boolean, - userContext: any = {} + shouldAttemptAccountLinkingIfAllowed?: boolean, + userContext?: Record ) { return Recipe.getInstanceOrThrowError().recipeInterfaceImpl.thirdPartyManuallyCreateOrUpdateUser({ thirdPartyId, @@ -56,38 +58,49 @@ export default class Wrapper { email, isVerified, tenantId, - userContext, + shouldAttemptAccountLinkingIfAllowed, + userContext: getUserContext(userContext), }); } - static emailPasswordSignUp(tenantId: string, email: string, password: string, userContext: any = {}) { + static emailPasswordSignUp(tenantId: string, email: string, password: string, userContext?: Record) { return Recipe.getInstanceOrThrowError().recipeInterfaceImpl.emailPasswordSignUp({ email, password, tenantId, - userContext, + userContext: getUserContext(userContext), }); } - static emailPasswordSignIn(tenantId: string, email: string, password: string, userContext: any = {}) { + static emailPasswordSignIn(tenantId: string, email: string, password: string, userContext?: Record) { return Recipe.getInstanceOrThrowError().recipeInterfaceImpl.emailPasswordSignIn({ email, password, tenantId, - userContext, + userContext: getUserContext(userContext), }); } - static createResetPasswordToken(tenantId: string, userId: string, email: string, userContext: any = {}) { + static createResetPasswordToken( + tenantId: string, + userId: string, + email: string, + userContext?: Record + ) { return Recipe.getInstanceOrThrowError().recipeInterfaceImpl.createResetPasswordToken({ userId, email, tenantId, - userContext, + userContext: getUserContext(userContext), }); } - static async resetPasswordUsingToken(tenantId: string, token: string, newPassword: string, userContext?: any) { + static async resetPasswordUsingToken( + tenantId: string, + token: string, + newPassword: string, + userContext?: Record + ) { const consumeResp = await Wrapper.consumePasswordResetToken(tenantId, token, userContext); if (consumeResp.status !== "OK") { @@ -103,11 +116,11 @@ export default class Wrapper { }); } - static consumePasswordResetToken(tenantId: string, token: string, userContext: any = {}) { + static consumePasswordResetToken(tenantId: string, token: string, userContext?: Record) { return Recipe.getInstanceOrThrowError().recipeInterfaceImpl.consumePasswordResetToken({ token, tenantId, - userContext, + userContext: getUserContext(userContext), }); } @@ -115,13 +128,13 @@ export default class Wrapper { recipeUserId: RecipeUserId; email?: string; password?: string; - userContext?: any; + userContext?: Record; applyPasswordPolicy?: boolean; tenantIdForPasswordPolicy?: string; }) { return Recipe.getInstanceOrThrowError().recipeInterfaceImpl.updateEmailOrPassword({ ...input, - userContext: input.userContext ?? {}, + userContext: getUserContext(input.userContext), tenantIdForPasswordPolicy: input.tenantIdForPasswordPolicy === undefined ? DEFAULT_TENANT_ID : input.tenantIdForPasswordPolicy, }); @@ -131,8 +144,9 @@ export default class Wrapper { tenantId: string, userId: string, email: string, - userContext: any = {} + userContext?: Record ): Promise<{ status: "OK"; link: string } | { status: "UNKNOWN_USER_ID_ERROR" }> { + const ctx = getUserContext(userContext); let token = await createResetPasswordToken(tenantId, userId, email, userContext); if (token.status === "UNKNOWN_USER_ID_ERROR") { return token; @@ -146,8 +160,8 @@ export default class Wrapper { recipeId: recipeInstance.getRecipeId(), token: token.token, tenantId, - request: getRequestFromUserContext(userContext), - userContext, + request: getRequestFromUserContext(ctx), + userContext: ctx, }), }; } @@ -156,7 +170,7 @@ export default class Wrapper { tenantId: string, userId: string, email: string, - userContext: any = {} + userContext?: Record ): Promise<{ status: "OK" | "UNKNOWN_USER_ID_ERROR" }> { const user = await getUser(userId, userContext); if (!user) { @@ -190,10 +204,10 @@ export default class Wrapper { }; } - static async sendEmail(input: TypeEmailPasswordEmailDeliveryInput & { userContext?: any }) { + static async sendEmail(input: TypeEmailPasswordEmailDeliveryInput & { userContext?: Record }) { return await Recipe.getInstanceOrThrowError().emailDelivery.ingredientInterfaceImpl.sendEmail({ ...input, - userContext: input.userContext ?? {}, + userContext: getUserContext(input.userContext), tenantId: input.tenantId === undefined ? DEFAULT_TENANT_ID : input.tenantId, }); } diff --git a/lib/ts/recipe/thirdpartyemailpassword/recipe.ts b/lib/ts/recipe/thirdpartyemailpassword/recipe.ts index a40a6bcdf..d507590ea 100644 --- a/lib/ts/recipe/thirdpartyemailpassword/recipe.ts +++ b/lib/ts/recipe/thirdpartyemailpassword/recipe.ts @@ -13,7 +13,7 @@ * under the License. */ import RecipeModule from "../../recipeModule"; -import { NormalisedAppinfo, APIHandled, RecipeListFunction, HTTPMethod } from "../../types"; +import { NormalisedAppinfo, APIHandled, RecipeListFunction, HTTPMethod, UserContext } from "../../types"; import EmailPasswordRecipe from "../emailpassword/recipe"; import ThirdPartyRecipe from "../thirdparty/recipe"; import type { BaseRequest, BaseResponse } from "../../framework"; @@ -204,7 +204,7 @@ export default class Recipe extends RecipeModule { res: BaseResponse, path: NormalisedURLPath, method: HTTPMethod, - userContext: any + userContext: UserContext ): Promise => { if ((await this.emailPasswordRecipe.returnAPIIdIfCanHandleRequest(path, method, userContext)) !== undefined) { return await this.emailPasswordRecipe.handleAPIRequest(id, tenantId, req, res, path, method, userContext); diff --git a/lib/ts/recipe/thirdpartyemailpassword/recipeImplementation/emailPasswordRecipeImplementation.ts b/lib/ts/recipe/thirdpartyemailpassword/recipeImplementation/emailPasswordRecipeImplementation.ts index 6dc8c29b9..6954f33f1 100644 --- a/lib/ts/recipe/thirdpartyemailpassword/recipeImplementation/emailPasswordRecipeImplementation.ts +++ b/lib/ts/recipe/thirdpartyemailpassword/recipeImplementation/emailPasswordRecipeImplementation.ts @@ -1,5 +1,5 @@ import { RecipeInterface } from "../../emailpassword/types"; -import { User } from "../../../types"; +import { User, UserContext } from "../../../types"; import { RecipeInterface as ThirdPartyEmailPasswordRecipeInterface } from "../types"; import RecipeUserId from "../../../recipeUserId"; @@ -9,10 +9,10 @@ export default function getRecipeInterface(recipeInterface: ThirdPartyEmailPassw email: string; password: string; tenantId: string; - userContext: any; + shouldAttemptAccountLinkingIfAllowed?: boolean; + userContext: UserContext; }): Promise< - | { status: "OK"; user: User; recipeUserId: RecipeUserId; isValidFirstFactorForTenant: boolean | undefined } - | { status: "EMAIL_ALREADY_EXISTS_ERROR" } + { status: "OK"; user: User; recipeUserId: RecipeUserId } | { status: "EMAIL_ALREADY_EXISTS_ERROR" } > { return await recipeInterface.emailPasswordSignUp(input); }, @@ -21,11 +21,8 @@ export default function getRecipeInterface(recipeInterface: ThirdPartyEmailPassw email: string; password: string; tenantId: string; - userContext: any; - }): Promise< - | { status: "OK"; user: User; recipeUserId: RecipeUserId; isValidFirstFactorForTenant: boolean | undefined } - | { status: "WRONG_CREDENTIALS_ERROR" } - > { + userContext: UserContext; + }): Promise<{ status: "OK"; user: User; recipeUserId: RecipeUserId } | { status: "WRONG_CREDENTIALS_ERROR" }> { return recipeInterface.emailPasswordSignIn(input); }, @@ -33,25 +30,28 @@ export default function getRecipeInterface(recipeInterface: ThirdPartyEmailPassw userId: string; email: string; tenantId: string; - userContext: any; + userContext: UserContext; }): Promise<{ status: "OK"; token: string } | { status: "UNKNOWN_USER_ID_ERROR" }> { return recipeInterface.createResetPasswordToken(input); }, - consumePasswordResetToken: async function (input: { token: string; tenantId: string; userContext: any }) { + consumePasswordResetToken: async function (input: { + token: string; + tenantId: string; + userContext: UserContext; + }) { return recipeInterface.consumePasswordResetToken(input); }, createNewRecipeUser: async function (input: { email: string; password: string; - userContext: any; + userContext: UserContext; }): Promise< | { status: "OK"; user: User; recipeUserId: RecipeUserId; - isValidFirstFactorForTenant: boolean | undefined; } | { status: "EMAIL_ALREADY_EXISTS_ERROR" } > { @@ -62,7 +62,7 @@ export default function getRecipeInterface(recipeInterface: ThirdPartyEmailPassw recipeUserId: RecipeUserId; email?: string; password?: string; - userContext: any; + userContext: UserContext; applyPasswordPolicy: boolean; tenantIdForPasswordPolicy: string; }): Promise< diff --git a/lib/ts/recipe/thirdpartyemailpassword/recipeImplementation/index.ts b/lib/ts/recipe/thirdpartyemailpassword/recipeImplementation/index.ts index b34eb2155..11227ecd6 100644 --- a/lib/ts/recipe/thirdpartyemailpassword/recipeImplementation/index.ts +++ b/lib/ts/recipe/thirdpartyemailpassword/recipeImplementation/index.ts @@ -7,7 +7,7 @@ import { Querier } from "../../../querier"; import DerivedEP from "./emailPasswordRecipeImplementation"; import DerivedTP from "./thirdPartyRecipeImplementation"; import { getUser } from "../../../"; -import { User } from "../../../types"; +import { User, UserContext } from "../../../types"; import RecipeUserId from "../../../recipeUserId"; import { TypeNormalisedInput } from "../../emailpassword/types"; @@ -30,10 +30,9 @@ export default function getRecipeInterface( email: string; password: string; tenantId: string; - userContext: any; + userContext: UserContext; }): Promise< - | { status: "OK"; user: User; recipeUserId: RecipeUserId; isValidFirstFactorForTenant: boolean | undefined } - | { status: "EMAIL_ALREADY_EXISTS_ERROR" } + { status: "OK"; user: User; recipeUserId: RecipeUserId } | { status: "EMAIL_ALREADY_EXISTS_ERROR" } > { return await originalEmailPasswordImplementation.createNewRecipeUser.bind(DerivedEP(this))(input); }, @@ -41,10 +40,10 @@ export default function getRecipeInterface( email: string; password: string; tenantId: string; - userContext: any; + shouldAttemptAccountLinkingIfAllowed?: boolean; + userContext: UserContext; }): Promise< - | { status: "OK"; user: User; recipeUserId: RecipeUserId; isValidFirstFactorForTenant: boolean | undefined } - | { status: "EMAIL_ALREADY_EXISTS_ERROR" } + { status: "OK"; user: User; recipeUserId: RecipeUserId } | { status: "EMAIL_ALREADY_EXISTS_ERROR" } > { return await originalEmailPasswordImplementation.signUp.bind(DerivedEP(this))(input); }, @@ -53,11 +52,8 @@ export default function getRecipeInterface( email: string; password: string; tenantId: string; - userContext: any; - }): Promise< - | { status: "OK"; user: User; recipeUserId: RecipeUserId; isValidFirstFactorForTenant: boolean | undefined } - | { status: "WRONG_CREDENTIALS_ERROR" } - > { + userContext: UserContext; + }): Promise<{ status: "OK"; user: User; recipeUserId: RecipeUserId } | { status: "WRONG_CREDENTIALS_ERROR" }> { return originalEmailPasswordImplementation.signIn.bind(DerivedEP(this))(input); }, @@ -72,7 +68,8 @@ export default function getRecipeInterface( fromUserInfoAPI?: { [key: string]: any }; }; tenantId: string; - userContext: any; + shouldAttemptAccountLinkingIfAllowed?: boolean; + userContext: UserContext; }): Promise< | { status: "OK"; @@ -84,7 +81,6 @@ export default function getRecipeInterface( fromIdTokenPayload?: { [key: string]: any }; fromUserInfoAPI?: { [key: string]: any }; }; - isValidFirstFactorForTenant: boolean | undefined; } | { status: "SIGN_IN_UP_NOT_ALLOWED"; @@ -100,14 +96,13 @@ export default function getRecipeInterface( email: string; isVerified: boolean; tenantId: string; - userContext: any; + userContext: UserContext; }): Promise< | { status: "OK"; createdNewRecipeUser: boolean; user: User; recipeUserId: RecipeUserId; - isValidFirstFactorForTenant: boolean | undefined; } | { status: "EMAIL_CHANGE_NOT_ALLOWED_ERROR"; @@ -125,7 +120,7 @@ export default function getRecipeInterface( thirdPartyId: string; clientType?: string; tenantId: string; - userContext: any; + userContext: UserContext; }): Promise { return originalThirdPartyImplementation.getProvider.bind(DerivedTP(this))(input); }, @@ -134,12 +129,16 @@ export default function getRecipeInterface( userId: string; email: string; tenantId: string; - userContext: any; + userContext: UserContext; }): Promise<{ status: "OK"; token: string } | { status: "UNKNOWN_USER_ID_ERROR" }> { return originalEmailPasswordImplementation.createResetPasswordToken.bind(DerivedEP(this))(input); }, - consumePasswordResetToken: async function (input: { token: string; tenantId: string; userContext: any }) { + consumePasswordResetToken: async function (input: { + token: string; + tenantId: string; + userContext: UserContext; + }) { return originalEmailPasswordImplementation.consumePasswordResetToken.bind(DerivedEP(this))(input); }, @@ -149,7 +148,7 @@ export default function getRecipeInterface( recipeUserId: RecipeUserId; email?: string; password?: string; - userContext: any; + userContext: UserContext; applyPasswordPolicy?: boolean; tenantIdForPasswordPolicy: string; } diff --git a/lib/ts/recipe/thirdpartyemailpassword/recipeImplementation/thirdPartyRecipeImplementation.ts b/lib/ts/recipe/thirdpartyemailpassword/recipeImplementation/thirdPartyRecipeImplementation.ts index 0caaa1b38..633eb40d8 100644 --- a/lib/ts/recipe/thirdpartyemailpassword/recipeImplementation/thirdPartyRecipeImplementation.ts +++ b/lib/ts/recipe/thirdpartyemailpassword/recipeImplementation/thirdPartyRecipeImplementation.ts @@ -1,6 +1,6 @@ import { RecipeInterface, TypeProvider } from "../../thirdparty/types"; import { RecipeInterface as ThirdPartyEmailPasswordRecipeInterface } from "../types"; -import { User } from "../../../types"; +import { User, UserContext } from "../../../types"; import RecipeUserId from "../../../recipeUserId"; export default function getRecipeInterface(recipeInterface: ThirdPartyEmailPasswordRecipeInterface): RecipeInterface { @@ -16,7 +16,8 @@ export default function getRecipeInterface(recipeInterface: ThirdPartyEmailPassw fromUserInfoAPI?: { [key: string]: any }; }; tenantId: string; - userContext: any; + shouldAttemptAccountLinkingIfAllowed?: boolean; + userContext: UserContext; }): Promise< | { status: "OK"; @@ -28,7 +29,6 @@ export default function getRecipeInterface(recipeInterface: ThirdPartyEmailPassw fromIdTokenPayload?: { [key: string]: any }; fromUserInfoAPI?: { [key: string]: any }; }; - isValidFirstFactorForTenant: boolean | undefined; } | { status: "SIGN_IN_UP_NOT_ALLOWED"; @@ -44,14 +44,14 @@ export default function getRecipeInterface(recipeInterface: ThirdPartyEmailPassw thirdPartyUserId: string; email: string; isVerified: boolean; - userContext: any; + shouldAttemptAccountLinkingIfAllowed?: boolean; + userContext: UserContext; }): Promise< | { status: "OK"; createdNewRecipeUser: boolean; user: User; recipeUserId: RecipeUserId; - isValidFirstFactorForTenant: boolean | undefined; } | { status: "EMAIL_CHANGE_NOT_ALLOWED_ERROR"; @@ -75,7 +75,6 @@ export default function getRecipeInterface(recipeInterface: ThirdPartyEmailPassw createdNewRecipeUser: result.createdNewRecipeUser, user: result.user, recipeUserId: result.recipeUserId, - isValidFirstFactorForTenant: result.isValidFirstFactorForTenant, }; }, @@ -83,7 +82,7 @@ export default function getRecipeInterface(recipeInterface: ThirdPartyEmailPassw thirdPartyId: string; clientType?: string; tenantId: string; - userContext: any; + userContext: UserContext; }): Promise { return await recipeInterface.thirdPartyGetProvider(input); }, diff --git a/lib/ts/recipe/thirdpartyemailpassword/types.ts b/lib/ts/recipe/thirdpartyemailpassword/types.ts index 8fa6e6d95..3618229c7 100644 --- a/lib/ts/recipe/thirdpartyemailpassword/types.ts +++ b/lib/ts/recipe/thirdpartyemailpassword/types.ts @@ -32,8 +32,9 @@ import { TypeInput as EmailDeliveryTypeInput, TypeInputWithService as EmailDeliveryTypeInputWithService, } from "../../ingredients/emaildelivery/types"; -import { GeneralErrorResponse, User as GlobalUser, User } from "../../types"; +import { GeneralErrorResponse, User as GlobalUser, User, UserContext } from "../../types"; import RecipeUserId from "../../recipeUserId"; +import { MFAFlowErrors } from "../multifactorauth/types"; export type TypeInputSignUp = { formFields?: TypeInputFormField[]; @@ -76,7 +77,7 @@ export type RecipeInterface = { thirdPartyId: string; clientType?: string; tenantId: string; - userContext: any; + userContext: UserContext; }): Promise; thirdPartySignInUp(input: { @@ -90,7 +91,8 @@ export type RecipeInterface = { fromUserInfoAPI?: { [key: string]: any }; }; tenantId: string; - userContext: any; + shouldAttemptAccountLinkingIfAllowed?: boolean; + userContext: UserContext; }): Promise< | { status: "OK"; @@ -102,7 +104,6 @@ export type RecipeInterface = { fromIdTokenPayload?: { [key: string]: any }; fromUserInfoAPI?: { [key: string]: any }; }; - isValidFirstFactorForTenant: boolean | undefined; } | { status: "SIGN_IN_UP_NOT_ALLOWED"; @@ -116,14 +117,14 @@ export type RecipeInterface = { email: string; isVerified: boolean; tenantId: string; - userContext: any; + shouldAttemptAccountLinkingIfAllowed?: boolean; + userContext: UserContext; }): Promise< | { status: "OK"; createdNewRecipeUser: boolean; user: GlobalUser; recipeUserId: RecipeUserId; - isValidFirstFactorForTenant: boolean | undefined; } | { status: "EMAIL_CHANGE_NOT_ALLOWED_ERROR"; @@ -139,13 +140,12 @@ export type RecipeInterface = { email: string; password: string; tenantId: string; - userContext: any; + userContext: UserContext; }): Promise< | { status: "OK"; user: GlobalUser; recipeUserId: RecipeUserId; - isValidFirstFactorForTenant: boolean | undefined; } | { status: "EMAIL_ALREADY_EXISTS_ERROR" } >; @@ -153,13 +153,12 @@ export type RecipeInterface = { createNewEmailPasswordRecipeUser(input: { email: string; password: string; - userContext: any; + userContext: UserContext; }): Promise< | { status: "OK"; user: GlobalUser; recipeUserId: RecipeUserId; - isValidFirstFactorForTenant: boolean | undefined; } | { status: "EMAIL_ALREADY_EXISTS_ERROR" } >; @@ -168,13 +167,12 @@ export type RecipeInterface = { email: string; password: string; tenantId: string; - userContext: any; + userContext: UserContext; }): Promise< | { status: "OK"; user: GlobalUser; recipeUserId: RecipeUserId; - isValidFirstFactorForTenant: boolean | undefined; } | { status: "WRONG_CREDENTIALS_ERROR" } >; @@ -183,13 +181,13 @@ export type RecipeInterface = { userId: string; email: string; tenantId: string; - userContext: any; + userContext: UserContext; }): Promise<{ status: "OK"; token: string } | { status: "UNKNOWN_USER_ID_ERROR" }>; consumePasswordResetToken(input: { token: string; tenantId: string; - userContext: any; + userContext: UserContext; }): Promise< | { status: "OK"; @@ -203,7 +201,7 @@ export type RecipeInterface = { recipeUserId: RecipeUserId; email?: string; password?: string; - userContext: any; + userContext: UserContext; applyPasswordPolicy?: boolean; tenantIdForPasswordPolicy: string; }): Promise< @@ -229,7 +227,7 @@ export type APIInterface = { redirectURIOnProviderDashboard: string; tenantId: string; options: ThirdPartyAPIOptions; - userContext: any; + userContext: UserContext; }) => Promise< | { status: "OK"; @@ -245,7 +243,7 @@ export type APIInterface = { email: string; tenantId: string; options: EmailPasswordAPIOptions; - userContext: any; + userContext: UserContext; }) => Promise< | { status: "OK"; @@ -263,7 +261,7 @@ export type APIInterface = { }[]; tenantId: string; options: EmailPasswordAPIOptions; - userContext: any; + userContext: UserContext; }) => Promise< | { status: "OK"; @@ -285,7 +283,7 @@ export type APIInterface = { token: string; tenantId: string; options: EmailPasswordAPIOptions; - userContext: any; + userContext: UserContext; }) => Promise< | { status: "OK"; @@ -306,7 +304,7 @@ export type APIInterface = { provider: TypeProvider; tenantId: string; options: ThirdPartyAPIOptions; - userContext: any; + userContext: UserContext; } & ( | { redirectURIInfo: { @@ -336,6 +334,7 @@ export type APIInterface = { status: "SIGN_IN_UP_NOT_ALLOWED"; reason: string; } + | MFAFlowErrors | GeneralErrorResponse >); @@ -348,7 +347,7 @@ export type APIInterface = { }[]; tenantId: string; options: EmailPasswordAPIOptions; - userContext: any; + userContext: UserContext; }) => Promise< | { status: "OK"; @@ -362,6 +361,7 @@ export type APIInterface = { | { status: "WRONG_CREDENTIALS_ERROR"; } + | MFAFlowErrors | GeneralErrorResponse >); @@ -374,7 +374,7 @@ export type APIInterface = { }[]; tenantId: string; options: EmailPasswordAPIOptions; - userContext: any; + userContext: UserContext; }) => Promise< | { status: "OK"; @@ -388,6 +388,7 @@ export type APIInterface = { | { status: "EMAIL_ALREADY_EXISTS_ERROR"; } + | MFAFlowErrors | GeneralErrorResponse >); @@ -396,7 +397,7 @@ export type APIInterface = { | ((input: { formPostInfoFromProvider: any; options: ThirdPartyAPIOptions; - userContext: any; + userContext: UserContext; }) => Promise); }; diff --git a/lib/ts/recipe/thirdpartypasswordless/emaildelivery/services/backwardCompatibility/index.ts b/lib/ts/recipe/thirdpartypasswordless/emaildelivery/services/backwardCompatibility/index.ts index ff8f183a0..daa99a00e 100644 --- a/lib/ts/recipe/thirdpartypasswordless/emaildelivery/services/backwardCompatibility/index.ts +++ b/lib/ts/recipe/thirdpartypasswordless/emaildelivery/services/backwardCompatibility/index.ts @@ -13,7 +13,7 @@ * under the License. */ import { TypeThirdPartyPasswordlessEmailDeliveryInput } from "../../../types"; -import { NormalisedAppinfo } from "../../../../../types"; +import { NormalisedAppinfo, UserContext } from "../../../../../types"; import PasswordlessBackwardCompatibilityService from "../../../../passwordless/emaildelivery/services/backwardCompatibility"; import { EmailDeliveryInterface } from "../../../../../ingredients/emaildelivery/types"; @@ -27,7 +27,7 @@ export default class BackwardCompatibilityService } } - sendEmail = async (input: TypeThirdPartyPasswordlessEmailDeliveryInput & { userContext: any }) => { + sendEmail = async (input: TypeThirdPartyPasswordlessEmailDeliveryInput & { userContext: UserContext }) => { await this.passwordlessBackwardCompatibilityService.sendEmail(input); }; } diff --git a/lib/ts/recipe/thirdpartypasswordless/emaildelivery/services/smtp/index.ts b/lib/ts/recipe/thirdpartypasswordless/emaildelivery/services/smtp/index.ts index f6e44b7ea..8892528df 100644 --- a/lib/ts/recipe/thirdpartypasswordless/emaildelivery/services/smtp/index.ts +++ b/lib/ts/recipe/thirdpartypasswordless/emaildelivery/services/smtp/index.ts @@ -20,6 +20,7 @@ import OverrideableBuilder from "supertokens-js-override"; import { getServiceImplementation } from "./serviceImplementation"; import PasswordlessSMTPService from "../../../../passwordless/emaildelivery/services/smtp"; import getPasswordlessServiceImplementation from "./serviceImplementation/passwordlessServiceImplementation"; +import { UserContext } from "../../../../../types"; export default class SMTPService implements EmailDeliveryInterface { serviceImpl: ServiceInterface; @@ -49,7 +50,7 @@ export default class SMTPService implements EmailDeliveryInterface { + sendEmail = async (input: TypeThirdPartyPasswordlessEmailDeliveryInput & { userContext: UserContext }) => { return await this.passwordlessSMTPService.sendEmail(input); }; } diff --git a/lib/ts/recipe/thirdpartypasswordless/emaildelivery/services/smtp/serviceImplementation/index.ts b/lib/ts/recipe/thirdpartypasswordless/emaildelivery/services/smtp/serviceImplementation/index.ts index 6cf7d5d81..f56e6a738 100644 --- a/lib/ts/recipe/thirdpartypasswordless/emaildelivery/services/smtp/serviceImplementation/index.ts +++ b/lib/ts/recipe/thirdpartypasswordless/emaildelivery/services/smtp/serviceImplementation/index.ts @@ -22,6 +22,7 @@ import { } from "../../../../../../ingredients/emaildelivery/services/smtp"; import { getServiceImplementation as getPasswordlessServiceImplementation } from "../../../../../passwordless/emaildelivery/services/smtp/serviceImplementation"; import DerivedPwdless from "./passwordlessServiceImplementation"; +import { UserContext } from "../../../../../../types"; export function getServiceImplementation( transporter: Transporter, @@ -41,7 +42,7 @@ export function getServiceImplementation( }); }, getContent: async function ( - input: TypeThirdPartyPasswordlessEmailDeliveryInput & { userContext: any } + input: TypeThirdPartyPasswordlessEmailDeliveryInput & { userContext: UserContext } ): Promise { return await passwordlessServiceImpl.getContent.bind(DerivedPwdless(this))(input); }, diff --git a/lib/ts/recipe/thirdpartypasswordless/emaildelivery/services/smtp/serviceImplementation/passwordlessServiceImplementation.ts b/lib/ts/recipe/thirdpartypasswordless/emaildelivery/services/smtp/serviceImplementation/passwordlessServiceImplementation.ts index 1f0ac04bf..5bcc1eea4 100644 --- a/lib/ts/recipe/thirdpartypasswordless/emaildelivery/services/smtp/serviceImplementation/passwordlessServiceImplementation.ts +++ b/lib/ts/recipe/thirdpartypasswordless/emaildelivery/services/smtp/serviceImplementation/passwordlessServiceImplementation.ts @@ -20,6 +20,7 @@ import { GetContentResult, } from "../../../../../../ingredients/emaildelivery/services/smtp"; import { TypePasswordlessEmailDeliveryInput } from "../../../../../passwordless/types"; +import { UserContext } from "../../../../../../types"; export default function getServiceInterface( thirdpartyPasswordlessServiceImplementation: ServiceInterface @@ -29,7 +30,7 @@ export default function getServiceInterface( return thirdpartyPasswordlessServiceImplementation.sendRawEmail(input); }, getContent: async function ( - input: TypePasswordlessEmailDeliveryInput & { userContext: any } + input: TypePasswordlessEmailDeliveryInput & { userContext: UserContext } ): Promise { return await thirdpartyPasswordlessServiceImplementation.getContent(input); }, diff --git a/lib/ts/recipe/thirdpartypasswordless/index.ts b/lib/ts/recipe/thirdpartypasswordless/index.ts index 2893d6bb9..29deddab6 100644 --- a/lib/ts/recipe/thirdpartypasswordless/index.ts +++ b/lib/ts/recipe/thirdpartypasswordless/index.ts @@ -26,6 +26,7 @@ import { TypeProvider } from "../thirdparty/types"; import { TypePasswordlessSmsDeliveryInput } from "../passwordless/types"; import RecipeUserId from "../../recipeUserId"; import { getRequestFromUserContext } from "../.."; +import { getUserContext } from "../../utils"; export default class Wrapper { static init = Recipe.init; @@ -36,13 +37,13 @@ export default class Wrapper { tenantId: string, thirdPartyId: string, clientType: string | undefined, - userContext: any = {} + userContext?: Record ) { return await Recipe.getInstanceOrThrowError().recipeInterfaceImpl.thirdPartyGetProvider({ thirdPartyId, tenantId, clientType, - userContext, + userContext: getUserContext(userContext), }); } @@ -52,7 +53,8 @@ export default class Wrapper { thirdPartyUserId: string, email: string, isVerified: boolean, - userContext: any = {} + shouldAttemptAccountLinkingIfAllowed?: boolean, + userContext?: Record ) { return Recipe.getInstanceOrThrowError().recipeInterfaceImpl.thirdPartyManuallyCreateOrUpdateUser({ thirdPartyId, @@ -60,7 +62,8 @@ export default class Wrapper { email, isVerified, tenantId, - userContext, + shouldAttemptAccountLinkingIfAllowed, + userContext: getUserContext(userContext), }); } @@ -72,11 +75,11 @@ export default class Wrapper { | { phoneNumber: string; } - ) & { userInputCode?: string; tenantId: string; userContext?: any } + ) & { userInputCode?: string; tenantId: string; userContext?: Record } ) { return Recipe.getInstanceOrThrowError().recipeInterfaceImpl.createCode({ ...input, - userContext: input.userContext ?? {}, + userContext: getUserContext(input.userContext), }); } @@ -84,11 +87,11 @@ export default class Wrapper { deviceId: string; userInputCode?: string; tenantId: string; - userContext?: any; + userContext?: Record; }) { return Recipe.getInstanceOrThrowError().recipeInterfaceImpl.createNewCodeForDevice({ ...input, - userContext: input.userContext ?? {}, + userContext: getUserContext(input.userContext), }); } @@ -99,18 +102,18 @@ export default class Wrapper { userInputCode: string; deviceId: string; tenantId: string; - userContext?: any; + userContext?: Record; } | { preAuthSessionId: string; linkCode: string; tenantId: string; - userContext?: any; + userContext?: Record; } ) { return Recipe.getInstanceOrThrowError().recipeInterfaceImpl.consumeCode({ ...input, - userContext: input.userContext ?? {}, + userContext: getUserContext(input.userContext), }); } @@ -118,11 +121,11 @@ export default class Wrapper { recipeUserId: RecipeUserId; email?: string | null; phoneNumber?: string | null; - userContext?: any; + userContext?: Record; }) { return Recipe.getInstanceOrThrowError().recipeInterfaceImpl.updatePasswordlessUser({ ...input, - userContext: input.userContext ?? {}, + userContext: getUserContext(input.userContext), }); } @@ -131,52 +134,56 @@ export default class Wrapper { | { email: string; tenantId: string; - userContext?: any; + userContext?: Record; } | { phoneNumber: string; tenantId: string; - userContext?: any; + userContext?: Record; } ) { return Recipe.getInstanceOrThrowError().recipeInterfaceImpl.revokeAllCodes({ ...input, - userContext: input.userContext ?? {}, + userContext: getUserContext(input.userContext), }); } - static revokeCode(input: { codeId: string; tenantId: string; userContext?: any }) { + static revokeCode(input: { codeId: string; tenantId: string; userContext?: Record }) { return Recipe.getInstanceOrThrowError().recipeInterfaceImpl.revokeCode({ ...input, - userContext: input.userContext ?? {}, + userContext: getUserContext(input.userContext), }); } - static listCodesByEmail(input: { email: string; tenantId: string; userContext?: any }) { + static listCodesByEmail(input: { email: string; tenantId: string; userContext?: Record }) { return Recipe.getInstanceOrThrowError().recipeInterfaceImpl.listCodesByEmail({ ...input, - userContext: input.userContext ?? {}, + userContext: getUserContext(input.userContext), }); } - static listCodesByPhoneNumber(input: { phoneNumber: string; tenantId: string; userContext?: any }) { + static listCodesByPhoneNumber(input: { phoneNumber: string; tenantId: string; userContext?: Record }) { return Recipe.getInstanceOrThrowError().recipeInterfaceImpl.listCodesByPhoneNumber({ ...input, - userContext: input.userContext ?? {}, + userContext: getUserContext(input.userContext), }); } - static listCodesByDeviceId(input: { deviceId: string; tenantId: string; userContext?: any }) { + static listCodesByDeviceId(input: { deviceId: string; tenantId: string; userContext?: Record }) { return Recipe.getInstanceOrThrowError().recipeInterfaceImpl.listCodesByDeviceId({ ...input, - userContext: input.userContext ?? {}, + userContext: getUserContext(input.userContext), }); } - static listCodesByPreAuthSessionId(input: { preAuthSessionId: string; tenantId: string; userContext?: any }) { + static listCodesByPreAuthSessionId(input: { + preAuthSessionId: string; + tenantId: string; + userContext?: Record; + }) { return Recipe.getInstanceOrThrowError().recipeInterfaceImpl.listCodesByPreAuthSessionId({ ...input, - userContext: input.userContext ?? {}, + userContext: getUserContext(input.userContext), }); } @@ -185,18 +192,19 @@ export default class Wrapper { | { email: string; tenantId: string; - userContext?: any; + userContext?: Record; } | { phoneNumber: string; tenantId: string; - userContext?: any; + userContext?: Record; } ) { + const ctx = getUserContext(input.userContext); return Recipe.getInstanceOrThrowError().passwordlessRecipe.createMagicLink({ ...input, - request: getRequestFromUserContext(input.userContext), - userContext: input.userContext ?? {}, + request: getRequestFromUserContext(ctx), + userContext: getUserContext(ctx), }); } @@ -205,31 +213,35 @@ export default class Wrapper { | { email: string; tenantId: string; - userContext?: any; + shouldAttemptAccountLinkingIfAllowed?: boolean; + userContext?: Record; } | { phoneNumber: string; tenantId: string; - userContext?: any; + shouldAttemptAccountLinkingIfAllowed?: boolean; + userContext?: Record; } ) { return Recipe.getInstanceOrThrowError().passwordlessRecipe.signInUp({ ...input, - userContext: input.userContext ?? {}, + userContext: getUserContext(input.userContext), }); } - static async sendEmail(input: TypeThirdPartyPasswordlessEmailDeliveryInput & { userContext?: any }) { + static async sendEmail( + input: TypeThirdPartyPasswordlessEmailDeliveryInput & { userContext?: Record } + ) { return await Recipe.getInstanceOrThrowError().emailDelivery.ingredientInterfaceImpl.sendEmail({ ...input, - userContext: input.userContext ?? {}, + userContext: getUserContext(input.userContext), }); } - static async sendSms(input: TypePasswordlessSmsDeliveryInput & { userContext?: any }) { + static async sendSms(input: TypePasswordlessSmsDeliveryInput & { userContext?: Record }) { return await Recipe.getInstanceOrThrowError().smsDelivery.ingredientInterfaceImpl.sendSms({ ...input, - userContext: input.userContext ?? {}, + userContext: getUserContext(input.userContext), }); } } diff --git a/lib/ts/recipe/thirdpartypasswordless/recipe.ts b/lib/ts/recipe/thirdpartypasswordless/recipe.ts index cc2f1922d..2a5e117f4 100644 --- a/lib/ts/recipe/thirdpartypasswordless/recipe.ts +++ b/lib/ts/recipe/thirdpartypasswordless/recipe.ts @@ -13,7 +13,7 @@ * under the License. */ import RecipeModule from "../../recipeModule"; -import { NormalisedAppinfo, APIHandled, RecipeListFunction, HTTPMethod } from "../../types"; +import { NormalisedAppinfo, APIHandled, RecipeListFunction, HTTPMethod, UserContext } from "../../types"; import PasswordlessRecipe from "../passwordless/recipe"; import ThirdPartyRecipe from "../thirdparty/recipe"; import type { BaseRequest, BaseResponse } from "../../framework"; @@ -210,7 +210,7 @@ export default class Recipe extends RecipeModule { res: BaseResponse, path: NormalisedURLPath, method: HTTPMethod, - userContext: any + userContext: UserContext ): Promise => { if ((await this.passwordlessRecipe.returnAPIIdIfCanHandleRequest(path, method, userContext)) !== undefined) { return await this.passwordlessRecipe.handleAPIRequest(id, tenantId, req, res, path, method, userContext); diff --git a/lib/ts/recipe/thirdpartypasswordless/recipeImplementation/index.ts b/lib/ts/recipe/thirdpartypasswordless/recipeImplementation/index.ts index 26a89470f..0c0cb6fb5 100644 --- a/lib/ts/recipe/thirdpartypasswordless/recipeImplementation/index.ts +++ b/lib/ts/recipe/thirdpartypasswordless/recipeImplementation/index.ts @@ -6,7 +6,7 @@ import { RecipeInterface as ThirdPartyRecipeInterface, TypeProvider } from "../. import { Querier } from "../../../querier"; import DerivedPwdless from "./passwordlessRecipeImplementation"; import DerivedTP from "./thirdPartyRecipeImplementation"; -import { User } from "../../../types"; +import { User, UserContext } from "../../../types"; import { RecipeUserId, getUser } from "../../../"; import { ProviderInput } from "../../thirdparty/types"; @@ -85,7 +85,8 @@ export default function getRecipeInterface( fromUserInfoAPI?: { [key: string]: any }; }; tenantId: string; - userContext: any; + shouldAttemptAccountLinkingIfAllowed?: boolean; + userContext: UserContext; }): Promise< | { status: "OK"; @@ -97,7 +98,6 @@ export default function getRecipeInterface( fromIdTokenPayload?: { [key: string]: any }; fromUserInfoAPI?: { [key: string]: any }; }; - isValidFirstFactorForTenant: boolean | undefined; } | { status: "SIGN_IN_UP_NOT_ALLOWED"; @@ -113,14 +113,13 @@ export default function getRecipeInterface( email: string; isVerified: boolean; tenantId: string; - userContext: any; + userContext: UserContext; }): Promise< | { status: "OK"; createdNewRecipeUser: boolean; user: User; recipeUserId: RecipeUserId; - isValidFirstFactorForTenant: boolean | undefined; } | { status: "EMAIL_CHANGE_NOT_ALLOWED_ERROR"; @@ -138,7 +137,7 @@ export default function getRecipeInterface( thirdPartyId: string; clientType?: string; tenantId: string; - userContext: any; + userContext: UserContext; }): Promise { return originalThirdPartyImplementation.getProvider.bind(DerivedTP(this))(input); }, diff --git a/lib/ts/recipe/thirdpartypasswordless/recipeImplementation/thirdPartyRecipeImplementation.ts b/lib/ts/recipe/thirdpartypasswordless/recipeImplementation/thirdPartyRecipeImplementation.ts index ed0073de7..37cfd96ac 100644 --- a/lib/ts/recipe/thirdpartypasswordless/recipeImplementation/thirdPartyRecipeImplementation.ts +++ b/lib/ts/recipe/thirdpartypasswordless/recipeImplementation/thirdPartyRecipeImplementation.ts @@ -1,6 +1,6 @@ import { RecipeInterface, TypeProvider } from "../../thirdparty/types"; import { RecipeInterface as ThirdPartyPasswordlessRecipeInterface } from "../types"; -import { User } from "../../../types"; +import { User, UserContext } from "../../../types"; import RecipeUserId from "../../../recipeUserId"; export default function getRecipeInterface(recipeInterface: ThirdPartyPasswordlessRecipeInterface): RecipeInterface { @@ -16,7 +16,8 @@ export default function getRecipeInterface(recipeInterface: ThirdPartyPasswordle fromUserInfoAPI?: { [key: string]: any }; }; tenantId: string; - userContext: any; + shouldAttemptAccountLinkingIfAllowed?: boolean; + userContext: UserContext; }): Promise< | { status: "OK"; @@ -28,7 +29,6 @@ export default function getRecipeInterface(recipeInterface: ThirdPartyPasswordle fromIdTokenPayload?: { [key: string]: any }; fromUserInfoAPI?: { [key: string]: any }; }; - isValidFirstFactorForTenant: boolean | undefined; } | { status: "SIGN_IN_UP_NOT_ALLOWED"; @@ -44,14 +44,14 @@ export default function getRecipeInterface(recipeInterface: ThirdPartyPasswordle email: string; isVerified: boolean; tenantId: string; - userContext: any; + shouldAttemptAccountLinkingIfAllowed?: boolean; + userContext: UserContext; }): Promise< | { status: "OK"; createdNewRecipeUser: boolean; user: User; recipeUserId: RecipeUserId; - isValidFirstFactorForTenant: boolean | undefined; } | { status: "EMAIL_CHANGE_NOT_ALLOWED_ERROR"; @@ -74,7 +74,6 @@ export default function getRecipeInterface(recipeInterface: ThirdPartyPasswordle createdNewRecipeUser: result.createdNewRecipeUser, recipeUserId: result.recipeUserId, user: result.user, - isValidFirstFactorForTenant: result.isValidFirstFactorForTenant, }; }, @@ -82,7 +81,7 @@ export default function getRecipeInterface(recipeInterface: ThirdPartyPasswordle thirdPartyId: string; clientType?: string; tenantId: string; - userContext: any; + userContext: UserContext; }): Promise { return await recipeInterface.thirdPartyGetProvider(input); }, diff --git a/lib/ts/recipe/thirdpartypasswordless/smsdelivery/services/backwardCompatibility/index.ts b/lib/ts/recipe/thirdpartypasswordless/smsdelivery/services/backwardCompatibility/index.ts index 7ccb6646a..13c5525ab 100644 --- a/lib/ts/recipe/thirdpartypasswordless/smsdelivery/services/backwardCompatibility/index.ts +++ b/lib/ts/recipe/thirdpartypasswordless/smsdelivery/services/backwardCompatibility/index.ts @@ -15,6 +15,7 @@ import { TypeThirdPartyPasswordlessSmsDeliveryInput } from "../../../types"; import { SmsDeliveryInterface } from "../../../../../ingredients/smsdelivery/types"; import PasswordlessBackwardCompatibilityService from "../../../../passwordless/smsdelivery/services/backwardCompatibility"; +import { UserContext } from "../../../../../types"; export default class BackwardCompatibilityService implements SmsDeliveryInterface { @@ -24,7 +25,7 @@ export default class BackwardCompatibilityService this.passwordlessBackwardCompatibilityService = new PasswordlessBackwardCompatibilityService(); } - sendSms = async (input: TypeThirdPartyPasswordlessSmsDeliveryInput & { userContext: any }) => { + sendSms = async (input: TypeThirdPartyPasswordlessSmsDeliveryInput & { userContext: UserContext }) => { await this.passwordlessBackwardCompatibilityService.sendSms(input); }; } diff --git a/lib/ts/recipe/thirdpartypasswordless/smsdelivery/services/supertokens/index.ts b/lib/ts/recipe/thirdpartypasswordless/smsdelivery/services/supertokens/index.ts index 303df7884..639c20101 100644 --- a/lib/ts/recipe/thirdpartypasswordless/smsdelivery/services/supertokens/index.ts +++ b/lib/ts/recipe/thirdpartypasswordless/smsdelivery/services/supertokens/index.ts @@ -15,6 +15,7 @@ import { SmsDeliveryInterface } from "../../../../../ingredients/smsdelivery/types"; import { TypeThirdPartyPasswordlessSmsDeliveryInput } from "../../../types"; import PasswordlessSupertokensService from "../../../../passwordless/smsdelivery/services/supertokens"; +import { UserContext } from "../../../../../types"; export default class SupertokensService implements SmsDeliveryInterface { private passwordlessSupertokensService: PasswordlessSupertokensService; @@ -23,7 +24,7 @@ export default class SupertokensService implements SmsDeliveryInterface { + sendSms = async (input: TypeThirdPartyPasswordlessSmsDeliveryInput & { userContext: UserContext }) => { await this.passwordlessSupertokensService.sendSms(input); }; } diff --git a/lib/ts/recipe/thirdpartypasswordless/smsdelivery/services/twilio/index.ts b/lib/ts/recipe/thirdpartypasswordless/smsdelivery/services/twilio/index.ts index bdf226d37..8b798292f 100644 --- a/lib/ts/recipe/thirdpartypasswordless/smsdelivery/services/twilio/index.ts +++ b/lib/ts/recipe/thirdpartypasswordless/smsdelivery/services/twilio/index.ts @@ -16,6 +16,7 @@ import { TypeInput } from "../../../../../ingredients/smsdelivery/services/twili import { SmsDeliveryInterface } from "../../../../../ingredients/smsdelivery/types"; import { TypeThirdPartyPasswordlessSmsDeliveryInput } from "../../../types"; import PasswordlessTwilioService from "../../../../passwordless/smsdelivery/services/twilio/index"; +import { UserContext } from "../../../../../types"; export default class TwilioService implements SmsDeliveryInterface { private passwordlessTwilioService: PasswordlessTwilioService; @@ -24,7 +25,7 @@ export default class TwilioService implements SmsDeliveryInterface { + sendSms = async (input: TypeThirdPartyPasswordlessSmsDeliveryInput & { userContext: UserContext }) => { await this.passwordlessTwilioService.sendSms(input); }; } diff --git a/lib/ts/recipe/thirdpartypasswordless/types.ts b/lib/ts/recipe/thirdpartypasswordless/types.ts index 42aed6ef6..baa876552 100644 --- a/lib/ts/recipe/thirdpartypasswordless/types.ts +++ b/lib/ts/recipe/thirdpartypasswordless/types.ts @@ -36,8 +36,9 @@ import { TypeInput as SmsDeliveryTypeInput, TypeInputWithService as SmsDeliveryTypeInputWithService, } from "../../ingredients/smsdelivery/types"; -import { GeneralErrorResponse, User } from "../../types"; +import { GeneralErrorResponse, User, UserContext } from "../../types"; import RecipeUserId from "../../recipeUserId"; +import { MFAFlowErrors } from "../multifactorauth/types"; export type DeviceType = DeviceTypeOriginal; @@ -69,7 +70,7 @@ export type TypeInput = ( // Override this to override how user input codes are generated // By default (=undefined) it is done in the Core - getCustomUserInputCode?: (tenantId: string, userContext: any) => Promise | string; + getCustomUserInputCode?: (tenantId: string, userContext: UserContext) => Promise | string; override?: { functions?: ( originalImplementation: RecipeInterface, @@ -104,7 +105,7 @@ export type TypeNormalisedInput = ( // Override this to override how user input codes are generated // By default (=undefined) it is done in the Core - getCustomUserInputCode?: (tenantId: string, userContext: any) => Promise | string; + getCustomUserInputCode?: (tenantId: string, userContext: UserContext) => Promise | string; providers: ProviderInput[]; getEmailDeliveryConfig: ( recipeImpl: RecipeInterface, @@ -132,7 +133,8 @@ export type RecipeInterface = { fromUserInfoAPI?: { [key: string]: any }; }; tenantId: string; - userContext: any; + shouldAttemptAccountLinkingIfAllowed?: boolean; + userContext: UserContext; }): Promise< | { status: "OK"; @@ -144,7 +146,6 @@ export type RecipeInterface = { fromIdTokenPayload?: { [key: string]: any }; fromUserInfoAPI?: { [key: string]: any }; }; - isValidFirstFactorForTenant: boolean | undefined; } | { status: "SIGN_IN_UP_NOT_ALLOWED"; @@ -158,14 +159,14 @@ export type RecipeInterface = { email: string; isVerified: boolean; tenantId: string; - userContext: any; + shouldAttemptAccountLinkingIfAllowed?: boolean; + userContext: UserContext; }): Promise< | { status: "OK"; createdNewRecipeUser: boolean; user: User; recipeUserId: RecipeUserId; - isValidFirstFactorForTenant: boolean | undefined; } | { status: "EMAIL_CHANGE_NOT_ALLOWED_ERROR"; @@ -181,7 +182,7 @@ export type RecipeInterface = { thirdPartyId: string; clientType?: string; tenantId: string; - userContext: any; + userContext: UserContext; }): Promise; createCode: ( @@ -192,7 +193,7 @@ export type RecipeInterface = { | { phoneNumber: string; } - ) & { userInputCode?: string; tenantId: string; userContext: any } + ) & { userInputCode?: string; tenantId: string; userContext: UserContext } ) => Promise<{ status: "OK"; preAuthSessionId: string; @@ -208,7 +209,7 @@ export type RecipeInterface = { deviceId: string; userInputCode?: string; tenantId: string; - userContext: any; + userContext: UserContext; }) => Promise< | { status: "OK"; @@ -230,13 +231,15 @@ export type RecipeInterface = { deviceId: string; preAuthSessionId: string; tenantId: string; - userContext: any; + shouldAttemptAccountLinkingIfAllowed?: boolean; + userContext: UserContext; } | { linkCode: string; preAuthSessionId: string; tenantId: string; - userContext: any; + shouldAttemptAccountLinkingIfAllowed?: boolean; + userContext: UserContext; } ) => Promise< | { @@ -244,7 +247,6 @@ export type RecipeInterface = { createdNewRecipeUser: boolean; user: User; recipeUserId: RecipeUserId; - isValidFirstFactorForTenant: boolean | undefined; } | { status: "INCORRECT_USER_INPUT_CODE_ERROR" | "EXPIRED_USER_INPUT_CODE_ERROR"; @@ -258,7 +260,7 @@ export type RecipeInterface = { recipeUserId: RecipeUserId; email?: string | null; phoneNumber?: string | null; - userContext: any; + userContext: UserContext; }) => Promise< | { status: @@ -278,12 +280,12 @@ export type RecipeInterface = { | { email: string; tenantId: string; - userContext: any; + userContext: UserContext; } | { phoneNumber: string; tenantId: string; - userContext: any; + userContext: UserContext; } ) => Promise<{ status: "OK"; @@ -292,29 +294,29 @@ export type RecipeInterface = { revokeCode: (input: { codeId: string; tenantId: string; - userContext: any; + userContext: UserContext; }) => Promise<{ status: "OK"; }>; - listCodesByEmail: (input: { email: string; tenantId: string; userContext: any }) => Promise; + listCodesByEmail: (input: { email: string; tenantId: string; userContext: UserContext }) => Promise; listCodesByPhoneNumber: (input: { phoneNumber: string; tenantId: string; - userContext: any; + userContext: UserContext; }) => Promise; listCodesByDeviceId: (input: { deviceId: string; tenantId: string; - userContext: any; + userContext: UserContext; }) => Promise; listCodesByPreAuthSessionId: (input: { preAuthSessionId: string; tenantId: string; - userContext: any; + userContext: UserContext; }) => Promise; }; @@ -329,7 +331,7 @@ export type APIInterface = { redirectURIOnProviderDashboard: string; tenantId: string; options: ThirdPartyAPIOptions; - userContext: any; + userContext: UserContext; }) => Promise< | { status: "OK"; @@ -346,7 +348,7 @@ export type APIInterface = { provider: TypeProvider; tenantId: string; options: ThirdPartyAPIOptions; - userContext: any; + userContext: UserContext; } & ( | { redirectURIInfo: { @@ -376,6 +378,7 @@ export type APIInterface = { status: "SIGN_IN_UP_NOT_ALLOWED"; reason: string; } + | MFAFlowErrors | GeneralErrorResponse >); @@ -384,7 +387,7 @@ export type APIInterface = { | ((input: { formPostInfoFromProvider: any; options: ThirdPartyAPIOptions; - userContext: any; + userContext: UserContext; }) => Promise); createCodePOST: @@ -393,7 +396,7 @@ export type APIInterface = { input: ({ email: string } | { phoneNumber: string }) & { tenantId: string; options: PasswordlessAPIOptions; - userContext: any; + userContext: UserContext; } ) => Promise< | { @@ -415,7 +418,7 @@ export type APIInterface = { input: { deviceId: string; preAuthSessionId: string } & { tenantId: string; options: PasswordlessAPIOptions; - userContext: any; + userContext: UserContext; } ) => Promise); @@ -435,7 +438,7 @@ export type APIInterface = { ) & { tenantId: string; options: PasswordlessAPIOptions; - userContext: any; + userContext: UserContext; } ) => Promise< | { @@ -449,12 +452,13 @@ export type APIInterface = { failedCodeInputAttemptCount: number; maximumCodeInputAttempts: number; } - | GeneralErrorResponse | { status: "RESTART_FLOW_ERROR" } | { status: "SIGN_IN_UP_NOT_ALLOWED"; reason: string; } + | GeneralErrorResponse + | MFAFlowErrors >); passwordlessUserEmailExistsGET: @@ -463,7 +467,7 @@ export type APIInterface = { email: string; tenantId: string; options: PasswordlessAPIOptions; - userContext: any; + userContext: UserContext; }) => Promise< | { status: "OK"; @@ -478,7 +482,7 @@ export type APIInterface = { phoneNumber: string; tenantId: string; options: PasswordlessAPIOptions; - userContext: any; + userContext: UserContext; }) => Promise< | { status: "OK"; diff --git a/lib/ts/recipe/totp/api/createDevice.ts b/lib/ts/recipe/totp/api/createDevice.ts index 145828885..30782ba25 100644 --- a/lib/ts/recipe/totp/api/createDevice.ts +++ b/lib/ts/recipe/totp/api/createDevice.ts @@ -16,11 +16,12 @@ import { send200Response } from "../../../utils"; import { APIInterface, APIOptions } from ".."; import Session from "../../session"; +import { UserContext } from "../../../types"; export default async function createDeviceAPI( apiImplementation: APIInterface, options: APIOptions, - userContext: any + userContext: UserContext ): Promise { if (apiImplementation.createDevicePOST === undefined) { return false; diff --git a/lib/ts/recipe/totp/api/implementation.ts b/lib/ts/recipe/totp/api/implementation.ts index 153dbf9b9..1fa6c1428 100644 --- a/lib/ts/recipe/totp/api/implementation.ts +++ b/lib/ts/recipe/totp/api/implementation.ts @@ -1,6 +1,7 @@ import { APIInterface } from "../"; import TotpRecipe from "../recipe"; import SessionError from "../../session/error"; +import MultiFactorAuthRecipe from "../../multifactorauth/recipe"; export default function getAPIInterface(): APIInterface { return { @@ -54,25 +55,88 @@ export default function getAPIInterface(): APIInterface { const userId = session.getUserId(); const tenantId = session.getTenantId(); - return await options.recipeImplementation.verifyDevice({ + const mfaInstance = MultiFactorAuthRecipe.getInstance(); + + if (mfaInstance === undefined) { + throw new Error("should never come here"); // TOTP can't work without MFA + } + + const validateMfaRes = await mfaInstance.validateForMultifactorAuthBeforeFactorCompletion({ + req: options.req, + res: options.res, + tenantId, + factorIdInProgress: "totp", + session, + userLoggingIn: undefined, + isAlreadySetup: false, // since this is a sign up + userContext, + }); + + if (validateMfaRes.status === "DISALLOWED_FIRST_FACTOR_ERROR") { + throw new Error("Should never come here"); // TOTP is never a first factor + } + + if (validateMfaRes.status !== "OK") { + return validateMfaRes; + } + + const res = await options.recipeImplementation.verifyDevice({ tenantId, userId, deviceName, totp, userContext, }); + + if (res.status === "OK") { + const sessionRes = await mfaInstance.createOrUpdateSessionForMultifactorAuthAfterFactorCompletion({ + req: options.req, + res: options.res, + tenantId, + factorIdInProgress: "totp", + isAlreadySetup: false, + userContext, + }); + if (sessionRes.status != "OK") { + return sessionRes; + } + } + + return res; }, verifyTOTPPOST: async function ({ totp, options, session, userContext }) { const userId = session.getUserId(); const tenantId = session.getTenantId(); - return await options.recipeImplementation.verifyTOTP({ + const mfaInstance = MultiFactorAuthRecipe.getInstance(); + if (mfaInstance === undefined) { + throw new Error("should never come here"); // TOTP can't work without MFA + } + + const res = await options.recipeImplementation.verifyTOTP({ tenantId, userId, totp, userContext, }); + + if (res.status === "OK") { + const sessionRes = await mfaInstance.createOrUpdateSessionForMultifactorAuthAfterFactorCompletion({ + req: options.req, + res: options.res, + tenantId, + factorIdInProgress: "totp", + isAlreadySetup: true, + userContext, + }); + + if (sessionRes.status != "OK") { + return sessionRes; + } + } + + return res; }, }; } diff --git a/lib/ts/recipe/totp/api/listDevices.ts b/lib/ts/recipe/totp/api/listDevices.ts index b00ecddaa..1901266b0 100644 --- a/lib/ts/recipe/totp/api/listDevices.ts +++ b/lib/ts/recipe/totp/api/listDevices.ts @@ -16,11 +16,12 @@ import { send200Response } from "../../../utils"; import { APIInterface, APIOptions } from ".."; import Session from "../../session"; +import { UserContext } from "../../../types"; export default async function listDevicesAPI( apiImplementation: APIInterface, options: APIOptions, - userContext: any + userContext: UserContext ): Promise { if (apiImplementation.listDevicesGET === undefined) { return false; diff --git a/lib/ts/recipe/totp/api/removeDevice.ts b/lib/ts/recipe/totp/api/removeDevice.ts index a03d0a209..474e00df0 100644 --- a/lib/ts/recipe/totp/api/removeDevice.ts +++ b/lib/ts/recipe/totp/api/removeDevice.ts @@ -16,11 +16,12 @@ import { send200Response } from "../../../utils"; import { APIInterface, APIOptions } from ".."; import Session from "../../session"; +import { UserContext } from "../../../types"; export default async function removeDeviceAPI( apiImplementation: APIInterface, options: APIOptions, - userContext: any + userContext: UserContext ): Promise { if (apiImplementation.removeDevicePOST === undefined) { return false; diff --git a/lib/ts/recipe/totp/api/verifyDevice.ts b/lib/ts/recipe/totp/api/verifyDevice.ts index ded698b7c..bfc75c4c2 100644 --- a/lib/ts/recipe/totp/api/verifyDevice.ts +++ b/lib/ts/recipe/totp/api/verifyDevice.ts @@ -16,11 +16,12 @@ import { send200Response } from "../../../utils"; import { APIInterface, APIOptions } from ".."; import Session from "../../session"; +import { UserContext } from "../../../types"; export default async function verifyDeviceAPI( apiImplementation: APIInterface, options: APIOptions, - userContext: any + userContext: UserContext ): Promise { if (apiImplementation.createDevicePOST === undefined) { return false; diff --git a/lib/ts/recipe/totp/api/verifyTOTP.ts b/lib/ts/recipe/totp/api/verifyTOTP.ts index 9573f6704..022725817 100644 --- a/lib/ts/recipe/totp/api/verifyTOTP.ts +++ b/lib/ts/recipe/totp/api/verifyTOTP.ts @@ -16,11 +16,12 @@ import { send200Response } from "../../../utils"; import { APIInterface, APIOptions } from ".."; import Session from "../../session"; +import { UserContext } from "../../../types"; export default async function verifyTOTPAPI( apiImplementation: APIInterface, options: APIOptions, - userContext: any + userContext: UserContext ): Promise { if (apiImplementation.createDevicePOST === undefined) { return false; diff --git a/lib/ts/recipe/totp/index.ts b/lib/ts/recipe/totp/index.ts index 72f8d1a66..5ee1b9da8 100644 --- a/lib/ts/recipe/totp/index.ts +++ b/lib/ts/recipe/totp/index.ts @@ -13,62 +13,80 @@ * under the License. */ +import { getUserContext } from "../../utils"; import Recipe from "./recipe"; import { RecipeInterface, APIOptions, APIInterface } from "./types"; export default class Wrapper { static init = Recipe.init; - static async createDevice(userId: string, deviceName?: string, skew?: number, period?: number, userContext?: any) { + static async createDevice( + userId: string, + deviceName?: string, + skew?: number, + period?: number, + userContext?: Record + ) { return Recipe.getInstanceOrThrowError().recipeInterfaceImpl.createDevice({ userId, deviceName, skew, period, - userContext: userContext ?? {}, + userContext: getUserContext(userContext), }); } - static async listDevices(userId: string, userContext?: any) { + static async listDevices(userId: string, userContext?: Record) { return Recipe.getInstanceOrThrowError().recipeInterfaceImpl.listDevices({ userId, - userContext: userContext ?? {}, + userContext: getUserContext(userContext), }); } - static async updateDevice(userId: string, existingDeviceName: string, newDeviceName: string, userContext?: any) { + static async updateDevice( + userId: string, + existingDeviceName: string, + newDeviceName: string, + userContext?: Record + ) { return Recipe.getInstanceOrThrowError().recipeInterfaceImpl.updateDevice({ userId, existingDeviceName, newDeviceName, - userContext: userContext ?? {}, + userContext: getUserContext(userContext), }); } - static async removeDevice(userId: string, deviceName: string, userContext?: any) { + static async removeDevice(userId: string, deviceName: string, userContext?: Record) { return Recipe.getInstanceOrThrowError().recipeInterfaceImpl.removeDevice({ userId, deviceName, - userContext: userContext ?? {}, + userContext: getUserContext(userContext), }); } - static async verifyDevice(tenantId: string, userId: string, deviceName: string, totp: string, userContext?: any) { + static async verifyDevice( + tenantId: string, + userId: string, + deviceName: string, + totp: string, + userContext?: Record + ) { return Recipe.getInstanceOrThrowError().recipeInterfaceImpl.verifyDevice({ tenantId, userId, deviceName, totp, - userContext: userContext ?? {}, + userContext: getUserContext(userContext), }); } - static async verifyTOTP(tenantId: string, userId: string, totp: string, userContext?: any) { + static async verifyTOTP(tenantId: string, userId: string, totp: string, userContext?: Record) { return Recipe.getInstanceOrThrowError().recipeInterfaceImpl.verifyTOTP({ tenantId, userId, totp, - userContext: userContext ?? {}, + userContext: getUserContext(userContext), }); } } diff --git a/lib/ts/recipe/totp/recipe.ts b/lib/ts/recipe/totp/recipe.ts index 9fd91aa6c..fe5fc1dab 100644 --- a/lib/ts/recipe/totp/recipe.ts +++ b/lib/ts/recipe/totp/recipe.ts @@ -19,7 +19,7 @@ import NormalisedURLPath from "../../normalisedURLPath"; import { Querier } from "../../querier"; import RecipeModule from "../../recipeModule"; import STError from "../../error"; -import { APIHandled, HTTPMethod, NormalisedAppinfo, RecipeListFunction } from "../../types"; +import { APIHandled, HTTPMethod, NormalisedAppinfo, RecipeListFunction, UserContext } from "../../types"; import RecipeImplementation from "./recipeImplementation"; import APIImplementation from "./api/implementation"; import { @@ -43,7 +43,10 @@ import verifyDeviceAPI from "./api/verifyDevice"; import verifyTOTPAPI from "./api/verifyTOTP"; import listDevicesAPI from "./api/listDevices"; import removeDeviceAPI from "./api/removeDevice"; -import { getUser } from "../.."; +import { User, getUser } from "../.."; +import { PostSuperTokensInitCallbacks } from "../../postSuperTokensInitCallbacks"; +import MultiFactorAuthRecipe from "../multifactorauth/recipe"; +import { TenantConfig } from "../multitenancy/types"; export default class Recipe extends RecipeModule { private static instance: Recipe | undefined = undefined; @@ -91,6 +94,43 @@ export default class Recipe extends RecipeModule { if (Recipe.instance === undefined) { Recipe.instance = new Recipe(Recipe.RECIPE_ID, appInfo, isInServerlessEnv, config); + PostSuperTokensInitCallbacks.addPostInitCallback(() => { + const mfaInstance = MultiFactorAuthRecipe.getInstance(); + if (mfaInstance !== undefined) { + mfaInstance.addGetAllFactorsFromOtherRecipesFunc((tenantConfig) => { + if (tenantConfig.totp.enabled === false) { + return { + factorIds: [], + firstFactorIds: [], + }; + } + return { + factorIds: ["totp"], + firstFactorIds: [], + }; + }); + mfaInstance.addGetFactorsSetupForUserFromOtherRecipes( + async (user: User, tenantConfig: TenantConfig, userContext: UserContext) => { + if (tenantConfig.totp.enabled === false) { + return []; + } + const deviceRes = await Recipe.getInstanceOrThrowError().recipeInterfaceImpl.listDevices( + { + userId: user.id, + userContext, + } + ); + for (const device of deviceRes.devices) { + if (device.verified) { + return ["totp"]; + } + } + return []; + } + ); + } + }); + return Recipe.instance; } else { throw new Error("TOTP recipe has already been initialised. Please check your code for bugs."); @@ -149,7 +189,7 @@ export default class Recipe extends RecipeModule { res: BaseResponse, _: NormalisedURLPath, __: HTTPMethod, - userContext: any + userContext: UserContext ): Promise => { let options = { recipeImplementation: this.recipeInterfaceImpl, diff --git a/lib/ts/recipe/totp/recipeImplementation.ts b/lib/ts/recipe/totp/recipeImplementation.ts index 44beaf805..306edaec4 100644 --- a/lib/ts/recipe/totp/recipeImplementation.ts +++ b/lib/ts/recipe/totp/recipeImplementation.ts @@ -2,6 +2,7 @@ import { RecipeInterface } from "./"; import { Querier } from "../../querier"; import NormalisedURLPath from "../../normalisedURLPath"; import { TypeNormalisedInput } from "./types"; +import { UserContext } from "../../types"; export default function getRecipeInterface(querier: Querier, config: TypeNormalisedInput): RecipeInterface { return { @@ -11,7 +12,7 @@ export default function getRecipeInterface(querier: Querier, config: TypeNormali skew?: number; period?: number; userIdentifierInfo?: string; - userContext: any; + userContext: UserContext; }) => { const response = await querier.sendPostRequest( new NormalisedURLPath("/recipe/totp/device"), @@ -20,7 +21,6 @@ export default function getRecipeInterface(querier: Querier, config: TypeNormali deviceName: input.deviceName, skew: input.skew ?? config.defaultSkew, period: input.period ?? config.defaultPeriod, - userContext: input.userContext, }, input.userContext ); @@ -41,7 +41,7 @@ export default function getRecipeInterface(querier: Querier, config: TypeNormali userId: string; existingDeviceName: string; newDeviceName: string; - userContext: any; + userContext: UserContext; }) => { return querier.sendPutRequest( new NormalisedURLPath("/recipe/totp/device"), @@ -49,30 +49,27 @@ export default function getRecipeInterface(querier: Querier, config: TypeNormali userId: input.userId, existingDeviceName: input.existingDeviceName, newDeviceName: input.newDeviceName, - userContext: input.userContext, }, input.userContext ); }, - listDevices: (input: { userId: string; userContext: any }) => { + listDevices: (input: { userId: string; userContext: UserContext }) => { return querier.sendGetRequest( new NormalisedURLPath("/recipe/totp/device/list"), { userId: input.userId, - userContext: input.userContext, }, input.userContext ); }, - removeDevice: (input: { userId: string; deviceName: string; userContext: any }) => { + removeDevice: (input: { userId: string; deviceName: string; userContext: UserContext }) => { return querier.sendPostRequest( new NormalisedURLPath("/recipe/totp/device/remove"), { userId: input.userId, deviceName: input.deviceName, - userContext: input.userContext, }, input.userContext ); @@ -83,7 +80,7 @@ export default function getRecipeInterface(querier: Querier, config: TypeNormali userId: string; deviceName: string; totp: string; - userContext: string; + userContext: UserContext; }) => { return querier.sendPostRequest( new NormalisedURLPath(`${input.tenantId}/recipe/totp/device/verify`), @@ -91,19 +88,17 @@ export default function getRecipeInterface(querier: Querier, config: TypeNormali userId: input.userId, deviceName: input.deviceName, totp: input.totp, - userContext: input.userContext, }, input.userContext ); }, - verifyTOTP: (input: { tenantId: string; userId: string; totp: string; userContext: any }) => { + verifyTOTP: (input: { tenantId: string; userId: string; totp: string; userContext: UserContext }) => { return querier.sendPostRequest( new NormalisedURLPath(`${input.tenantId}/recipe/totp/verify`), { userId: input.userId, totp: input.totp, - userContext: input.userContext, }, input.userContext ); diff --git a/lib/ts/recipe/totp/types.ts b/lib/ts/recipe/totp/types.ts index b870b646e..921783ddd 100644 --- a/lib/ts/recipe/totp/types.ts +++ b/lib/ts/recipe/totp/types.ts @@ -15,12 +15,13 @@ import { BaseRequest, BaseResponse } from "../../framework"; import OverrideableBuilder from "supertokens-js-override"; -import { GeneralErrorResponse } from "../../types"; +import { GeneralErrorResponse, UserContext } from "../../types"; import { SessionContainerInterface } from "../session/types"; +import { MFAFlowErrors } from "../multifactorauth/types"; export type GetUserIdentifierInfoForUserIdFunc = ( userId: string, - userContext: any + userContext: UserContext ) => Promise< | { status: "OK"; @@ -68,7 +69,7 @@ export type RecipeInterface = { deviceName?: string; skew?: number; period?: number; - userContext: any; + userContext: UserContext; }) => Promise< | { status: "OK"; @@ -84,13 +85,13 @@ export type RecipeInterface = { userId: string; existingDeviceName: string; newDeviceName: string; - userContext: any; + userContext: UserContext; }) => Promise<{ status: "OK" | "UNKNOWN_DEVICE_ERROR" | "DEVICE_ALREADY_EXISTS_ERROR"; }>; listDevices: (input: { userId: string; - userContext: any; + userContext: UserContext; }) => Promise<{ status: "OK"; devices: { @@ -103,7 +104,7 @@ export type RecipeInterface = { removeDevice: (input: { userId: string; deviceName: string; - userContext: any; + userContext: UserContext; }) => Promise<{ status: "OK"; didDeviceExist: boolean; @@ -113,7 +114,7 @@ export type RecipeInterface = { userId: string; deviceName: string; totp: string; - userContext: string; + userContext: UserContext; }) => Promise< | { status: "OK"; @@ -136,7 +137,7 @@ export type RecipeInterface = { tenantId: string; userId: string; totp: string; - userContext: any; + userContext: UserContext; }) => Promise< | { status: "OK" | "UNKNOWN_USER_ID_ERROR"; @@ -167,7 +168,7 @@ export type APIInterface = { deviceName?: string; options: APIOptions; session: SessionContainerInterface; - userContext: any; + userContext: UserContext; }) => Promise< | { status: "OK" | "DEVICE_ALREADY_EXISTS_ERROR"; @@ -184,7 +185,7 @@ export type APIInterface = { listDevicesGET: (input: { options: APIOptions; session: SessionContainerInterface; - userContext: any; + userContext: UserContext; }) => Promise< | { status: "OK"; @@ -202,7 +203,7 @@ export type APIInterface = { deviceName: string; options: APIOptions; session: SessionContainerInterface; - userContext: any; + userContext: UserContext; }) => Promise< | { status: "OK"; @@ -216,7 +217,7 @@ export type APIInterface = { totp: string; options: APIOptions; session: SessionContainerInterface; - userContext: any; + userContext: UserContext; }) => Promise< | { status: "OK"; @@ -234,6 +235,10 @@ export type APIInterface = { status: "LIMIT_REACHED_ERROR"; retryAfterMs: number; } + | { + status: "FACTOR_SETUP_NOT_ALLOWED_ERROR"; + } + | MFAFlowErrors | GeneralErrorResponse >; @@ -241,7 +246,7 @@ export type APIInterface = { totp: string; options: APIOptions; session: SessionContainerInterface; - userContext: any; + userContext: UserContext; }) => Promise< | { status: "OK" | "UNKNOWN_USER_ID_ERROR"; @@ -255,6 +260,7 @@ export type APIInterface = { status: "LIMIT_REACHED_ERROR"; retryAfterMs: number; } + | MFAFlowErrors | GeneralErrorResponse >; }; diff --git a/lib/ts/recipe/usermetadata/index.ts b/lib/ts/recipe/usermetadata/index.ts index 5148c96da..3ac182399 100644 --- a/lib/ts/recipe/usermetadata/index.ts +++ b/lib/ts/recipe/usermetadata/index.ts @@ -14,31 +14,32 @@ */ import { JSONObject } from "../../types"; +import { getUserContext } from "../../utils"; import Recipe from "./recipe"; import { RecipeInterface } from "./types"; export default class Wrapper { static init = Recipe.init; - static async getUserMetadata(userId: string, userContext?: any) { + static async getUserMetadata(userId: string, userContext?: Record) { return await Recipe.getInstanceOrThrowError().recipeInterfaceImpl.getUserMetadata({ userId, - userContext: userContext === undefined ? {} : userContext, + userContext: getUserContext(userContext), }); } - static async updateUserMetadata(userId: string, metadataUpdate: JSONObject, userContext?: any) { + static async updateUserMetadata(userId: string, metadataUpdate: JSONObject, userContext?: Record) { return await Recipe.getInstanceOrThrowError().recipeInterfaceImpl.updateUserMetadata({ userId, metadataUpdate, - userContext: userContext === undefined ? {} : userContext, + userContext: getUserContext(userContext), }); } - static async clearUserMetadata(userId: string, userContext?: any) { + static async clearUserMetadata(userId: string, userContext?: Record) { return await Recipe.getInstanceOrThrowError().recipeInterfaceImpl.clearUserMetadata({ userId, - userContext: userContext === undefined ? {} : userContext, + userContext: getUserContext(userContext), }); } } diff --git a/lib/ts/recipe/usermetadata/recipeImplementation.ts b/lib/ts/recipe/usermetadata/recipeImplementation.ts index 83292cf3e..925a92f85 100644 --- a/lib/ts/recipe/usermetadata/recipeImplementation.ts +++ b/lib/ts/recipe/usermetadata/recipeImplementation.ts @@ -35,10 +35,14 @@ export default function getRecipeInterface(querier: Querier): RecipeInterface { }, updateUserMetadataInternal: function ({ userId, metadataUpdate, userContext }) { - return querier.sendPutRequest(new NormalisedURLPath("/recipe/user/metadata"), { - userId, - metadataUpdate, - }, userContext); + return querier.sendPutRequest( + new NormalisedURLPath("/recipe/user/metadata"), + { + userId, + metadataUpdate, + }, + userContext + ); }, clearUserMetadata: function ({ userId, userContext }) { diff --git a/lib/ts/recipe/usermetadata/types.ts b/lib/ts/recipe/usermetadata/types.ts index 07f090f1e..acd82558f 100644 --- a/lib/ts/recipe/usermetadata/types.ts +++ b/lib/ts/recipe/usermetadata/types.ts @@ -14,7 +14,7 @@ */ import OverrideableBuilder from "supertokens-js-override"; -import { JSONObject } from "../../types"; +import { JSONObject, UserContext } from "../../types"; export type TypeInput = { override?: { @@ -41,7 +41,7 @@ export type APIInterface = {}; export type RecipeInterface = { getUserMetadata: (input: { userId: string; - userContext: any; + userContext: UserContext; }) => Promise<{ status: "OK"; metadata: any; @@ -58,7 +58,7 @@ export type RecipeInterface = { updateUserMetadata: (input: { userId: string; metadataUpdate: JSONObject; - userContext: any; + userContext: UserContext; }) => Promise<{ status: "OK"; metadata: JSONObject; @@ -68,7 +68,7 @@ export type RecipeInterface = { updateUserMetadataInternal: (input: { userId: string; metadataUpdate: JSONObject; - userContext: any; + userContext: UserContext; }) => Promise<{ status: "OK"; metadata: JSONObject; @@ -76,7 +76,7 @@ export type RecipeInterface = { clearUserMetadata: (input: { userId: string; - userContext: any; + userContext: UserContext; }) => Promise<{ status: "OK"; }>; diff --git a/lib/ts/recipe/userroles/index.ts b/lib/ts/recipe/userroles/index.ts index 59681dab6..2ddaafe2b 100644 --- a/lib/ts/recipe/userroles/index.ts +++ b/lib/ts/recipe/userroles/index.ts @@ -13,6 +13,7 @@ * under the License. */ +import { getUserContext } from "../../utils"; import { PermissionClaim } from "./permissionClaim"; import Recipe from "./recipe"; import { RecipeInterface } from "./types"; @@ -23,80 +24,80 @@ export default class Wrapper { static PermissionClaim = PermissionClaim; static UserRoleClaim = UserRoleClaim; - static async addRoleToUser(tenantId: string, userId: string, role: string, userContext?: any) { + static async addRoleToUser(tenantId: string, userId: string, role: string, userContext?: Record) { return await Recipe.getInstanceOrThrowError().recipeInterfaceImpl.addRoleToUser({ userId, role, tenantId, - userContext: userContext === undefined ? {} : userContext, + userContext: getUserContext(userContext), }); } - static async removeUserRole(tenantId: string, userId: string, role: string, userContext?: any) { + static async removeUserRole(tenantId: string, userId: string, role: string, userContext?: Record) { return await Recipe.getInstanceOrThrowError().recipeInterfaceImpl.removeUserRole({ userId, role, tenantId, - userContext: userContext === undefined ? {} : userContext, + userContext: getUserContext(userContext), }); } - static async getRolesForUser(tenantId: string, userId: string, userContext?: any) { + static async getRolesForUser(tenantId: string, userId: string, userContext?: Record) { return await Recipe.getInstanceOrThrowError().recipeInterfaceImpl.getRolesForUser({ userId, tenantId, - userContext: userContext === undefined ? {} : userContext, + userContext: getUserContext(userContext), }); } - static async getUsersThatHaveRole(tenantId: string, role: string, userContext?: any) { + static async getUsersThatHaveRole(tenantId: string, role: string, userContext?: Record) { return await Recipe.getInstanceOrThrowError().recipeInterfaceImpl.getUsersThatHaveRole({ role, tenantId, - userContext: userContext === undefined ? {} : userContext, + userContext: getUserContext(userContext), }); } - static async createNewRoleOrAddPermissions(role: string, permissions: string[], userContext?: any) { + static async createNewRoleOrAddPermissions(role: string, permissions: string[], userContext?: Record) { return await Recipe.getInstanceOrThrowError().recipeInterfaceImpl.createNewRoleOrAddPermissions({ role, permissions, - userContext: userContext === undefined ? {} : userContext, + userContext: getUserContext(userContext), }); } - static async getPermissionsForRole(role: string, userContext?: any) { + static async getPermissionsForRole(role: string, userContext?: Record) { return await Recipe.getInstanceOrThrowError().recipeInterfaceImpl.getPermissionsForRole({ role, - userContext: userContext === undefined ? {} : userContext, + userContext: getUserContext(userContext), }); } - static async removePermissionsFromRole(role: string, permissions: string[], userContext?: any) { + static async removePermissionsFromRole(role: string, permissions: string[], userContext?: Record) { return await Recipe.getInstanceOrThrowError().recipeInterfaceImpl.removePermissionsFromRole({ role, permissions, - userContext: userContext === undefined ? {} : userContext, + userContext: getUserContext(userContext), }); } - static async getRolesThatHavePermission(permission: string, userContext?: any) { + static async getRolesThatHavePermission(permission: string, userContext?: Record) { return await Recipe.getInstanceOrThrowError().recipeInterfaceImpl.getRolesThatHavePermission({ permission, - userContext: userContext === undefined ? {} : userContext, + userContext: getUserContext(userContext), }); } - static async deleteRole(role: string, userContext?: any) { + static async deleteRole(role: string, userContext?: Record) { return await Recipe.getInstanceOrThrowError().recipeInterfaceImpl.deleteRole({ role, - userContext: userContext === undefined ? {} : userContext, + userContext: getUserContext(userContext), }); } - static async getAllRoles(userContext?: any) { + static async getAllRoles(userContext?: Record) { return await Recipe.getInstanceOrThrowError().recipeInterfaceImpl.getAllRoles({ - userContext: userContext === undefined ? {} : userContext, + userContext: getUserContext(userContext), }); } } diff --git a/lib/ts/recipe/userroles/permissionClaim.ts b/lib/ts/recipe/userroles/permissionClaim.ts index 7fa66a77d..02a506b6f 100644 --- a/lib/ts/recipe/userroles/permissionClaim.ts +++ b/lib/ts/recipe/userroles/permissionClaim.ts @@ -8,7 +8,7 @@ export class PermissionClaimClass extends PrimitiveArrayClaim { constructor() { super({ key: "st-perm", - async fetchValue(userId, _recipeUserId, tenantId, userContext) { + async fetchValue(userId, _recipeUserId, tenantId, _currentPayload, userContext) { const recipe = UserRoleRecipe.getInstanceOrThrowError(); // We fetch the roles because the rolesClaim may not be present in the payload diff --git a/lib/ts/recipe/userroles/recipeImplementation.ts b/lib/ts/recipe/userroles/recipeImplementation.ts index 60e9f8590..eaee083f7 100644 --- a/lib/ts/recipe/userroles/recipeImplementation.ts +++ b/lib/ts/recipe/userroles/recipeImplementation.ts @@ -85,7 +85,7 @@ export default function getRecipeInterface(querier: Querier): RecipeInterface { return querier.sendPostRequest(new NormalisedURLPath("/recipe/role/remove"), { role }, userContext); }, - getAllRoles: function (userContext) { + getAllRoles: function ({ userContext }) { return querier.sendGetRequest(new NormalisedURLPath("/recipe/roles"), {}, userContext); }, }; diff --git a/lib/ts/recipe/userroles/types.ts b/lib/ts/recipe/userroles/types.ts index 84e9d1aef..191fbb01c 100644 --- a/lib/ts/recipe/userroles/types.ts +++ b/lib/ts/recipe/userroles/types.ts @@ -14,6 +14,7 @@ */ import OverrideableBuilder from "supertokens-js-override"; +import { UserContext } from "../../types"; export type TypeInput = { skipAddingRolesToAccessToken?: boolean; @@ -46,7 +47,7 @@ export type RecipeInterface = { userId: string; role: string; tenantId: string; - userContext: any; + userContext: UserContext; }) => Promise< | { status: "OK"; @@ -61,7 +62,7 @@ export type RecipeInterface = { userId: string; role: string; tenantId: string; - userContext: any; + userContext: UserContext; }) => Promise< | { status: "OK"; @@ -75,7 +76,7 @@ export type RecipeInterface = { getRolesForUser: (input: { userId: string; tenantId: string; - userContext: any; + userContext: UserContext; }) => Promise<{ status: "OK"; roles: string[]; @@ -84,7 +85,7 @@ export type RecipeInterface = { getUsersThatHaveRole: (input: { role: string; tenantId: string; - userContext: any; + userContext: UserContext; }) => Promise< | { status: "OK"; @@ -98,7 +99,7 @@ export type RecipeInterface = { createNewRoleOrAddPermissions: (input: { role: string; permissions: string[]; - userContext: any; + userContext: UserContext; }) => Promise<{ status: "OK"; createdNewRole: boolean; @@ -106,7 +107,7 @@ export type RecipeInterface = { getPermissionsForRole: (input: { role: string; - userContext: any; + userContext: UserContext; }) => Promise< | { status: "OK"; @@ -120,14 +121,14 @@ export type RecipeInterface = { removePermissionsFromRole: (input: { role: string; permissions: string[]; - userContext: any; + userContext: UserContext; }) => Promise<{ status: "OK" | "UNKNOWN_ROLE_ERROR"; }>; getRolesThatHavePermission: (input: { permission: string; - userContext: any; + userContext: UserContext; }) => Promise<{ status: "OK"; roles: string[]; @@ -135,14 +136,14 @@ export type RecipeInterface = { deleteRole: (input: { role: string; - userContext: any; + userContext: UserContext; }) => Promise<{ status: "OK"; didRoleExist: boolean; }>; getAllRoles: (input: { - userContext: any; + userContext: UserContext; }) => Promise<{ status: "OK"; roles: string[]; diff --git a/lib/ts/recipe/userroles/userRoleClaim.ts b/lib/ts/recipe/userroles/userRoleClaim.ts index e7f689bad..06603511b 100644 --- a/lib/ts/recipe/userroles/userRoleClaim.ts +++ b/lib/ts/recipe/userroles/userRoleClaim.ts @@ -8,7 +8,7 @@ export class UserRoleClaimClass extends PrimitiveArrayClaim { constructor() { super({ key: "st-role", - async fetchValue(userId, _recipeUserId, tenantId, userContext) { + async fetchValue(userId, _recipeUserId, tenantId, _currentPayload, userContext) { const recipe = UserRoleRecipe.getInstanceOrThrowError(); const res = await recipe.recipeInterfaceImpl.getRolesForUser({ userId, diff --git a/lib/ts/recipeModule.ts b/lib/ts/recipeModule.ts index b55331950..3b73692c8 100644 --- a/lib/ts/recipeModule.ts +++ b/lib/ts/recipeModule.ts @@ -14,7 +14,7 @@ */ import STError from "./error"; -import { NormalisedAppinfo, APIHandled, HTTPMethod } from "./types"; +import { NormalisedAppinfo, APIHandled, HTTPMethod, UserContext } from "./types"; import NormalisedURLPath from "./normalisedURLPath"; import { BaseRequest, BaseResponse } from "./framework"; import { DEFAULT_TENANT_ID } from "./recipe/multitenancy/constants"; @@ -40,7 +40,7 @@ export default abstract class RecipeModule { returnAPIIdIfCanHandleRequest = async ( path: NormalisedURLPath, method: HTTPMethod, - userContext: any + userContext: UserContext ): Promise<{ id: string; tenantId: string } | undefined> => { let apisHandled = this.getAPIsHandled(); @@ -98,10 +98,15 @@ export default abstract class RecipeModule { response: BaseResponse, path: NormalisedURLPath, method: HTTPMethod, - userContext: any + userContext: UserContext ): Promise; - abstract handleError(error: STError, request: BaseRequest, response: BaseResponse, userContext: any): Promise; + abstract handleError( + error: STError, + request: BaseRequest, + response: BaseResponse, + userContext: UserContext + ): Promise; abstract getAllCORSHeaders(): string[]; diff --git a/lib/ts/supertokens.ts b/lib/ts/supertokens.ts index a97651de6..322081588 100644 --- a/lib/ts/supertokens.ts +++ b/lib/ts/supertokens.ts @@ -13,7 +13,7 @@ * under the License. */ -import { TypeInput, NormalisedAppinfo, HTTPMethod, SuperTokensInfo } from "./types"; +import { TypeInput, NormalisedAppinfo, HTTPMethod, SuperTokensInfo, UserContext } from "./types"; import { normaliseInputAppInfoOrThrowError, maxVersion, @@ -99,6 +99,7 @@ export default class SuperTokens { this.isInServerlessEnv = config.isInServerlessEnv === undefined ? false : config.isInServerlessEnv; let multitenancyFound = false; + let totpFound = false; let userMetadataFound = false; let multiFactorAuthFound = false; @@ -107,7 +108,9 @@ export default class SuperTokens { // between `supertokens.ts` -> `recipeModule.ts` -> `multitenancy/recipe.ts` let MultitenancyRecipe = require("./recipe/multitenancy/recipe").default; let UserMetadataRecipe = require("./recipe/usermetadata/recipe").default; - let MultiFactorAuthRecipe = require("./recipe/multiFactorAuth/recipe").default; + let MultiFactorAuthRecipe = require("./recipe/multifactorauth/recipe").default; + let TotpRecipe = require("./recipe/totp/recipe").default; + this.recipeModules = config.recipeList.map((func) => { const recipeModule = func(this.appInfo, this.isInServerlessEnv); if (recipeModule.getRecipeId() === MultitenancyRecipe.RECIPE_ID) { @@ -116,6 +119,8 @@ export default class SuperTokens { userMetadataFound = true; } else if (recipeModule.getRecipeId() === MultiFactorAuthRecipe.RECIPE_ID) { multiFactorAuthFound = true; + } else if (recipeModule.getRecipeId() === TotpRecipe.RECIPE_ID) { + totpFound = true; } return recipeModule; }); @@ -123,6 +128,11 @@ export default class SuperTokens { if (!multitenancyFound) { this.recipeModules.push(MultitenancyRecipe.init()(this.appInfo, this.isInServerlessEnv)); } + if (totpFound && !multiFactorAuthFound) { + // we want MFA to be enabled if totp is enabled + this.recipeModules.push(MultiFactorAuthRecipe.init()(this.appInfo, this.isInServerlessEnv)); + multiFactorAuthFound = true; + } if (multiFactorAuthFound && !userMetadataFound) { // we want user metadata to be initialized if MFA is enabled this.recipeModules.push(UserMetadataRecipe.init()(this.appInfo, this.isInServerlessEnv)); @@ -161,7 +171,7 @@ export default class SuperTokens { response: BaseResponse, path: NormalisedURLPath, method: HTTPMethod, - userContext: any + userContext: UserContext ) => { return await matchedRecipe.handleAPIRequest(id, tenantId, request, response, path, method, userContext); }; @@ -179,7 +189,11 @@ export default class SuperTokens { return Array.from(headerSet); }; - getUserCount = async (includeRecipeIds?: string[], tenantId?: string, userContext?: any): Promise => { + getUserCount = async ( + includeRecipeIds: string[] | undefined, + tenantId: string | undefined, + userContext: UserContext + ): Promise => { let querier = Querier.getNewInstanceOrThrowError(undefined); let apiVersion = await querier.getAPIVersion(); if (maxVersion(apiVersion, "2.7") === "2.7") { @@ -198,7 +212,7 @@ export default class SuperTokens { includeRecipeIds: includeRecipeIdsStr, includeAllTenants: tenantId === undefined, }, - userContext === undefined ? {} : userContext + userContext ); return Number(response.count); }; @@ -208,7 +222,7 @@ export default class SuperTokens { externalUserId: string; externalUserIdInfo?: string; force?: boolean; - userContext?: any; + userContext: UserContext; }): Promise< | { status: "OK" | "UNKNOWN_SUPERTOKENS_USER_ID_ERROR"; @@ -231,7 +245,7 @@ export default class SuperTokens { externalUserIdInfo: input.externalUserIdInfo, force: input.force, }, - input.userContext === undefined ? {} : input.userContext + input.userContext ); } else { throw new global.Error("Please upgrade the SuperTokens core to >= 3.15.0"); @@ -241,7 +255,7 @@ export default class SuperTokens { getUserIdMapping = async function (input: { userId: string; userIdType?: "SUPERTOKENS" | "EXTERNAL" | "ANY"; - userContext?: any; + userContext: UserContext; }): Promise< | { status: "OK"; @@ -263,7 +277,7 @@ export default class SuperTokens { userId: input.userId, userIdType: input.userIdType, }, - input.userContext === undefined ? {} : input.userContext + input.userContext ); return response; } else { @@ -275,7 +289,7 @@ export default class SuperTokens { userId: string; userIdType?: "SUPERTOKENS" | "EXTERNAL" | "ANY"; force?: boolean; - userContext?: any; + userContext: UserContext; }): Promise<{ status: "OK"; didMappingExist: boolean; @@ -290,7 +304,7 @@ export default class SuperTokens { userIdType: input.userIdType, force: input.force, }, - input.userContext === undefined ? {} : input.userContext + input.userContext ); } else { throw new global.Error("Please upgrade the SuperTokens core to >= 3.15.0"); @@ -301,7 +315,7 @@ export default class SuperTokens { userId: string; userIdType?: "SUPERTOKENS" | "EXTERNAL" | "ANY"; externalUserIdInfo?: string; - userContext?: any; + userContext: UserContext; }): Promise<{ status: "OK" | "UNKNOWN_MAPPING_ERROR"; }> { @@ -315,14 +329,14 @@ export default class SuperTokens { userIdType: input.userIdType, externalUserIdInfo: input.externalUserIdInfo, }, - input.userContext === undefined ? {} : input.userContext + input.userContext ); } else { throw new global.Error("Please upgrade the SuperTokens core to >= 3.15.0"); } }; - middleware = async (request: BaseRequest, response: BaseResponse, userContext: any): Promise => { + middleware = async (request: BaseRequest, response: BaseResponse, userContext: UserContext): Promise => { logDebugMessage("middleware: Started"); let path = this.appInfo.apiGatewayPath.appendPath(new NormalisedURLPath(request.getOriginalURL())); let method: HTTPMethod = normaliseHttpMethod(request.getMethod()); @@ -420,7 +434,7 @@ export default class SuperTokens { } }; - errorHandler = async (err: any, request: BaseRequest, response: BaseResponse, userContext: any) => { + errorHandler = async (err: any, request: BaseRequest, response: BaseResponse, userContext: UserContext) => { logDebugMessage("errorHandler: Started"); if (STError.isErrorFromSuperTokens(err)) { logDebugMessage("errorHandler: Error is from SuperTokens recipe. Message: " + err.message); @@ -440,7 +454,7 @@ export default class SuperTokens { throw err; }; - getRequestFromUserContext = (userContext: any | undefined): BaseRequest | undefined => { + getRequestFromUserContext = (userContext: UserContext | undefined): BaseRequest | undefined => { if (userContext === undefined) { return undefined; } diff --git a/lib/ts/types.ts b/lib/ts/types.ts index 4fc906b82..c6a062602 100644 --- a/lib/ts/types.ts +++ b/lib/ts/types.ts @@ -19,11 +19,17 @@ import NormalisedURLPath from "./normalisedURLPath"; import { TypeFramework } from "./framework/types"; import { RecipeLevelUser } from "./recipe/accountlinking/types"; import { BaseRequest } from "./framework"; +declare const __brand: unique symbol; +type Brand = { [__brand]: B }; + +type Branded = T & Brand; + +export type UserContext = Branded, "UserContext">; export type AppInfo = { appName: string; websiteDomain?: string; - origin?: string | ((input: { request: BaseRequest | undefined; userContext: any }) => string); + origin?: string | ((input: { request: BaseRequest | undefined; userContext: UserContext }) => string); websiteBasePath?: string; apiDomain: string; apiBasePath?: string; @@ -32,10 +38,10 @@ export type AppInfo = { export type NormalisedAppinfo = { appName: string; - getOrigin: (input: { request: BaseRequest | undefined; userContext: any }) => NormalisedURLDomain; + getOrigin: (input: { request: BaseRequest | undefined; userContext: UserContext }) => NormalisedURLDomain; apiDomain: NormalisedURLDomain; topLevelAPIDomain: string; - getTopLevelWebsiteDomain: (input: { request: BaseRequest | undefined; userContext: any }) => string; + getTopLevelWebsiteDomain: (input: { request: BaseRequest | undefined; userContext: UserContext }) => string; apiBasePath: NormalisedURLPath; apiGatewayPath: NormalisedURLPath; websiteBasePath: NormalisedURLPath; @@ -57,7 +63,7 @@ export type TypeInput = { debug?: boolean; }; -export type NetworkInterceptor = (request: HttpRequest, userContext: any) => HttpRequest; +export type NetworkInterceptor = (request: HttpRequest, userContext: UserContext) => HttpRequest; export interface HttpRequest { url: string; diff --git a/lib/ts/utils.ts b/lib/ts/utils.ts index f60552408..2f0e3b73d 100644 --- a/lib/ts/utils.ts +++ b/lib/ts/utils.ts @@ -1,6 +1,6 @@ import * as psl from "psl"; -import type { AppInfo, NormalisedAppinfo, HTTPMethod, JSONObject } from "./types"; +import type { AppInfo, NormalisedAppinfo, HTTPMethod, JSONObject, UserContext } from "./types"; import NormalisedURLDomain from "./normalisedURLDomain"; import NormalisedURLPath from "./normalisedURLPath"; import type { BaseRequest, BaseResponse } from "./framework"; @@ -70,7 +70,7 @@ export function normaliseInputAppInfoOrThrowError(appInfo: AppInfo): NormalisedA ); } - let websiteDomainFunction = (input: { request: BaseRequest | undefined; userContext: any }) => { + let websiteDomainFunction = (input: { request: BaseRequest | undefined; userContext: UserContext }) => { let origin = appInfo.origin; if (origin === undefined) { @@ -90,7 +90,7 @@ export function normaliseInputAppInfoOrThrowError(appInfo: AppInfo): NormalisedA const apiDomain = new NormalisedURLDomain(appInfo.apiDomain); const topLevelAPIDomain = getTopLevelDomainForSameSiteResolution(apiDomain.getAsStringDangerous()); - const topLevelWebsiteDomain = (input: { request: BaseRequest | undefined; userContext: any }) => { + const topLevelWebsiteDomain = (input: { request: BaseRequest | undefined; userContext: UserContext }) => { return getTopLevelDomainForSameSiteResolution(websiteDomainFunction(input).getAsStringDangerous()); }; @@ -266,13 +266,17 @@ export function humaniseMilliseconds(ms: number): string { } } -export function makeDefaultUserContextFromAPI(request: BaseRequest): any { - return setRequestInUserContextIfNotDefined({}, request); +export function makeDefaultUserContextFromAPI(request: BaseRequest): UserContext { + return setRequestInUserContextIfNotDefined({} as UserContext, request); } -export function setRequestInUserContextIfNotDefined(userContext: any | undefined, request: BaseRequest) { +export function getUserContext(inputUserContext?: Record): UserContext { + return (inputUserContext ?? {}) as UserContext; +} + +export function setRequestInUserContextIfNotDefined(userContext: UserContext | undefined, request: BaseRequest) { if (userContext === undefined) { - userContext = {}; + userContext = {} as UserContext; } if (userContext._default === undefined) { diff --git a/lib/ts/version.ts b/lib/ts/version.ts index d60d298ac..04e958f52 100644 --- a/lib/ts/version.ts +++ b/lib/ts/version.ts @@ -14,7 +14,7 @@ */ export const version = "16.6.5"; -export const cdiSupported = ["4.1"]; +export const cdiSupported = ["5.0"]; // Note: The actual script import for dashboard uses v{DASHBOARD_VERSION} export const dashboardVersion = "0.9"; diff --git a/test/accountlinking/emailpassword.test.js b/test/accountlinking/emailpassword.test.js index 8cc69d075..33290344c 100644 --- a/test/accountlinking/emailpassword.test.js +++ b/test/accountlinking/emailpassword.test.js @@ -394,7 +394,7 @@ describe(`accountlinkingTests: ${printPath("[test/accountlinking/emailpassword.t }); let user = ( - await EmailPassword.signUp("public", "test@example.com", "password123", { + await EmailPassword.signUp("public", "test@example.com", "password123", undefined, { doNotLink: true, }) ).user; @@ -498,7 +498,8 @@ describe(`accountlinkingTests: ${printPath("[test/accountlinking/emailpassword.t const email1 = `test+${Date.now()}@example.com`; let user = (await EmailPassword.signUp("public", email1, "password123")).user; const email2 = `test+${Date.now()}@example.com`; - let user2 = (await EmailPassword.signUp("public", email2, "password123", { doNotLink: true })).user; + let user2 = (await EmailPassword.signUp("public", email2, "password123", undefined, { doNotLink: true })) + .user; const linkResp = await AccountLinking.linkAccounts(user2.loginMethods[0].recipeUserId, user.id); assert.strictEqual(linkResp.status, "OK"); diff --git a/test/accountlinking/emailpasswordapis.test.js b/test/accountlinking/emailpasswordapis.test.js index 95e7154dc..b40ba6cff 100644 --- a/test/accountlinking/emailpasswordapis.test.js +++ b/test/accountlinking/emailpasswordapis.test.js @@ -274,6 +274,7 @@ describe(`accountlinkingTests: ${printPath("[test/accountlinking/emailpasswordap "abc", "test@example.com", true, + undefined, { doNotLink: true, } @@ -666,6 +667,7 @@ describe(`accountlinkingTests: ${printPath("[test/accountlinking/emailpasswordap "abc", "test@example.com", false, + undefined, { doNotLink: true, } @@ -1187,7 +1189,7 @@ describe(`accountlinkingTests: ${printPath("[test/accountlinking/emailpasswordap app.use(middleware()); app.use(errorHandler()); - let epUser = await EmailPassword.signUp("public", "test@example.com", "password123", { + let epUser = await EmailPassword.signUp("public", "test@example.com", "password123", undefined, { doNotLink: true, }); assert(!epUser.user.isPrimaryUser); diff --git a/test/accountlinking/emailpasswordapis2.test.js b/test/accountlinking/emailpasswordapis2.test.js index 031841743..60ce285fa 100644 --- a/test/accountlinking/emailpasswordapis2.test.js +++ b/test/accountlinking/emailpasswordapis2.test.js @@ -1230,6 +1230,7 @@ describe(`accountlinkingTests: ${printPath("[test/accountlinking/emailpasswordap "abc", "test2@example.com", false, + undefined, { doNotLink: true, } @@ -1444,7 +1445,7 @@ describe(`accountlinkingTests: ${printPath("[test/accountlinking/emailpasswordap ); assert(tpUser.isPrimaryUser); - let epUser = await EmailPassword.signUp("public", "test@example.com", "password1234", { + let epUser = await EmailPassword.signUp("public", "test@example.com", "password1234", undefined, { doNotLink: true, }); assert(epUser.user.isPrimaryUser === false); @@ -2660,7 +2661,7 @@ describe(`accountlinkingTests: ${printPath("[test/accountlinking/emailpasswordap assert(res.body.status === "OK"); assert(sendEmailToUserId === tpUser.id); - let epUser = await EmailPassword.signUp("public", "test@example.com", "password1234", { + let epUser = await EmailPassword.signUp("public", "test@example.com", "password1234", undefined, { doNotLink: true, }); diff --git a/test/accountlinking/helperFunctions.test.js b/test/accountlinking/helperFunctions.test.js index c44c0726b..728429903 100644 --- a/test/accountlinking/helperFunctions.test.js +++ b/test/accountlinking/helperFunctions.test.js @@ -123,7 +123,7 @@ describe(`accountlinkingTests: ${printPath("[test/accountlinking/helperFunctions }); let user = ( - await EmailPassword.signUp("public", "test@example.com", "password123", { + await EmailPassword.signUp("public", "test@example.com", "password123", undefined, { doNotLink: true, }) ).user; @@ -300,9 +300,17 @@ describe(`accountlinkingTests: ${printPath("[test/accountlinking/helperFunctions }); let primaryUser = ( - await ThirdParty.manuallyCreateOrUpdateUser("public", "google", "abc", "test@example.com", false, { - doNotLink: true, - }) + await ThirdParty.manuallyCreateOrUpdateUser( + "public", + "google", + "abc", + "test@example.com", + false, + undefined, + { + doNotLink: true, + } + ) ).user; assert(primaryUser.isPrimaryUser === false); @@ -310,7 +318,7 @@ describe(`accountlinkingTests: ${printPath("[test/accountlinking/helperFunctions await AccountLinking.createPrimaryUser(supertokens.convertToRecipeUserId(primaryUser.id)); let user = ( - await EmailPassword.signUp("public", "test@example.com", "password123", { + await EmailPassword.signUp("public", "test@example.com", "password123", undefined, { doNotLink: true, }) ).user; @@ -1492,7 +1500,7 @@ describe(`accountlinkingTests: ${printPath("[test/accountlinking/helperFunctions ); assert(user.isPrimaryUser); - let response = await EmailPassword.signUp("public", "test2@example.com", "password123", { + let response = await EmailPassword.signUp("public", "test2@example.com", "password123", undefined, { doNotLink: true, }); assert(response.user.isPrimaryUser === false); @@ -2689,9 +2697,17 @@ describe(`accountlinkingTests: ${printPath("[test/accountlinking/helperFunctions await EmailVerification.verifyEmailUsingToken("public", token.token); let tpUser = ( - await ThirdParty.manuallyCreateOrUpdateUser("public", "abcd", "abcd", "test@example.com", true, { - doNotLink: true, - }) + await ThirdParty.manuallyCreateOrUpdateUser( + "public", + "abcd", + "abcd", + "test@example.com", + true, + undefined, + { + doNotLink: true, + } + ) ).user; assert(tpUser.isPrimaryUser === false); assert(tpUser.loginMethods[0].verified); diff --git a/test/accountlinking/thirdpartyapis.test.js b/test/accountlinking/thirdpartyapis.test.js index 11e9f79ba..d8f2eb3c0 100644 --- a/test/accountlinking/thirdpartyapis.test.js +++ b/test/accountlinking/thirdpartyapis.test.js @@ -534,9 +534,17 @@ describe(`accountlinkingTests: ${printPath("[test/accountlinking/thirdpartyapis. await AccountLinking.createPrimaryUser(tpUser.loginMethods[0].recipeUserId); let tpUser2 = ( - await ThirdParty.manuallyCreateOrUpdateUser("public", "custom-ev", "user", "email@test.com", true, { - doNotLink: true, - }) + await ThirdParty.manuallyCreateOrUpdateUser( + "public", + "custom-ev", + "user", + "email@test.com", + true, + undefined, + { + doNotLink: true, + } + ) ).user; assert(tpUser2.isPrimaryUser === false); @@ -943,9 +951,17 @@ describe(`accountlinkingTests: ${printPath("[test/accountlinking/thirdpartyapis. nock("https://test.com").post("/oauth/token").reply(200, {}); let tpUser = ( - await ThirdParty.manuallyCreateOrUpdateUser("public", "custom-no-ev", "user", "email2@test.com", true, { - doNotLink: true, - }) + await ThirdParty.manuallyCreateOrUpdateUser( + "public", + "custom-no-ev", + "user", + "email2@test.com", + true, + undefined, + { + doNotLink: true, + } + ) ).user; assert(tpUser.isPrimaryUser === false); @@ -1144,9 +1160,17 @@ describe(`accountlinkingTests: ${printPath("[test/accountlinking/thirdpartyapis. nock("https://test.com").post("/oauth/token").reply(200, {}); let tpUser = ( - await ThirdParty.manuallyCreateOrUpdateUser("public", "custom-no-ev", "user", "email@test.com", true, { - doNotLink: true, - }) + await ThirdParty.manuallyCreateOrUpdateUser( + "public", + "custom-no-ev", + "user", + "email@test.com", + true, + undefined, + { + doNotLink: true, + } + ) ).user; assert(tpUser.isPrimaryUser === false); diff --git a/test/accountlinking/userstructure.test.js b/test/accountlinking/userstructure.test.js index c40cc3baa..a9fa34ffe 100644 --- a/test/accountlinking/userstructure.test.js +++ b/test/accountlinking/userstructure.test.js @@ -258,6 +258,7 @@ describe(`accountlinkingTests: ${printPath("[test/accountlinking/userstructure.t }, }), Session.init({ + overwriteSessionDuringSignIn: true, override: { functions: (oI) => { return { diff --git a/test/framework/crossframework/repeatedResponseHeader.test.js b/test/framework/crossframework/repeatedResponseHeader.test.js index fa6702515..304516e4a 100644 --- a/test/framework/crossframework/repeatedResponseHeader.test.js +++ b/test/framework/crossframework/repeatedResponseHeader.test.js @@ -22,7 +22,7 @@ addCrossFrameworkTests( { path: "/create", method: "post", - handler: async (req, res, next) => { + handler: async (req, res, _, next) => { await Session.createNewSession( req, res, @@ -40,7 +40,7 @@ addCrossFrameworkTests( path: "/session/multipleMerge", method: "post", verifySession: true, - handler: async (req, res, session) => { + handler: async (req, res, session, _) => { await session.mergeIntoAccessTokenPayload({ test1: Date.now() }); await session.mergeIntoAccessTokenPayload({ test2: Date.now() }); await session.mergeIntoAccessTokenPayload({ test3: Date.now() }); diff --git a/test/framework/crossframework/unauthorised.test.js b/test/framework/crossframework/unauthorised.test.js index e5589dea4..4c24a42f9 100644 --- a/test/framework/crossframework/unauthorised.test.js +++ b/test/framework/crossframework/unauthorised.test.js @@ -41,7 +41,7 @@ addCrossFrameworkTests( { path: "/create", method: "post", - handler: async (req, res, next) => { + handler: async (req, res, _, next) => { await Session.createNewSession( req, res, diff --git a/test/jwt/getJWKS.test.js b/test/jwt/getJWKS.test.js index 5da6558a0..1ec845e8b 100644 --- a/test/jwt/getJWKS.test.js +++ b/test/jwt/getJWKS.test.js @@ -138,7 +138,7 @@ describe(`getJWKS: ${printPath("[test/jwt/getJWKS.test.js]")}`, function () { functions: (oI) => ({ ...oI, getJWKS: async (input) => { - const res = await oI.getJWKS(); + const res = await oI.getJWKS(input); return { ...res, validityInSeconds: 1234, @@ -198,7 +198,7 @@ describe(`getJWKS: ${printPath("[test/jwt/getJWKS.test.js]")}`, function () { functions: (oI) => ({ ...oI, getJWKS: async (input) => { - const res = await oI.getJWKS(); + const res = await oI.getJWKS(input); return { ...res, validityInSeconds: undefined, diff --git a/test/jwt/override.test.js b/test/jwt/override.test.js index d9176d604..f7585c68b 100644 --- a/test/jwt/override.test.js +++ b/test/jwt/override.test.js @@ -52,8 +52,8 @@ describe(`overrideTest: ${printPath("[test/jwt/override.test.js]")}`, function ( return createJWTResponse; }, - getJWKS: async () => { - let getJWKSResponse = await originalImplementation.getJWKS(); + getJWKS: async (input) => { + let getJWKSResponse = await originalImplementation.getJWKS(input); jwksKeys = getJWKSResponse.keys; diff --git a/test/mfa/mfa.api.test.js b/test/mfa/mfa.api.test.js index d61fd7fb4..7b0bfe145 100644 --- a/test/mfa/mfa.api.test.js +++ b/test/mfa/mfa.api.test.js @@ -1,4 +1,11 @@ -const { printPath, setupST, startSTWithMultitenancy, killAllST, cleanST } = require("../utils"); +const { + printPath, + setupST, + startSTWithMultitenancy, + killAllST, + cleanST, + extractInfoFromResponse, +} = require("../utils"); let assert = require("assert"); const express = require("express"); let { ProcessState } = require("../../lib/build/processState"); @@ -15,6 +22,17 @@ let ThirdParty = require("../../lib/build/recipe/thirdparty"); let Multitenancy = require("../../lib/build/recipe/multitenancy"); let AccountLinking = require("../../lib/build/recipe/accountlinking"); const OTPAuth = require("otpauth"); +const { json } = require("body-parser"); +const request = require("supertest"); +const { + epSignIn, + epSignUp, + plessEmailSignInUp, + plessPhoneSigninUp, + tpSignInUp, + getMfaInfo, + getTestExpressApp, +} = require("./utils"); describe(`mfa-api: ${printPath("[test/mfa/mfa.api.test.js]")}`, function () { beforeEach(async function () { @@ -27,4 +45,304 @@ describe(`mfa-api: ${printPath("[test/mfa/mfa.api.test.js]")}`, function () { await killAllST(); await cleanST(); }); + + it("test mfa info after first factor", async function () { + const connectionURI = await startSTWithMultitenancy(); + SuperTokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + EmailPassword.init(), + Passwordless.init({ + contactMethod: "EMAIL", + flowType: "USER_INPUT_CODE", + }), + ThirdParty.init(), + Totp.init(), + MultiFactorAuth.init(), + Session.init(), + ], + }); + + const app = getTestExpressApp(); + + await EmailPassword.signUp("public", "test@example.com", "password"); + + let res = await epSignIn(app, "test@example.com", "password"); + assert.equal("OK", res.body.status); + + let cookies = extractInfoFromResponse(res); + const accessToken = cookies.accessTokenFromAny; + + res = await getMfaInfo(app, accessToken); + assert.equal("OK", res.body.status); + assert.equal("test@example.com", res.body.email); + assert.deepEqual(["emailpassword", "otp-email"], res.body.factors.isAlreadySetup); + assert.deepEqual(["emailpassword", "otp-email", "thirdparty", "totp"], res.body.factors.isAllowedToSetup); + + res = await plessEmailSignInUp(app, "test@example.com", accessToken); + assert.equal("OK", res.body.status); + // the users must have been account linked now + assert.equal(true, res.body.user.isPrimaryUser); + assert.equal(2, res.body.user.loginMethods.length); + + res = await getMfaInfo(app, accessToken); + assert.equal("OK", res.body.status); + assert.equal("test@example.com", res.body.email); + assert.deepEqual(["emailpassword", "otp-email"], res.body.factors.isAlreadySetup); + assert.deepEqual(["emailpassword", "otp-email", "thirdparty", "totp"], res.body.factors.isAllowedToSetup); + }); + + it("test that only a valid first factor is allowed to login", async function () { + const connectionURI = await startSTWithMultitenancy(); + SuperTokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + EmailPassword.init(), + Passwordless.init({ + contactMethod: "EMAIL", + flowType: "USER_INPUT_CODE", + }), + ThirdParty.init(), + Totp.init(), + MultiFactorAuth.init({ + firstFactors: ["emailpassword"], + }), + Session.init(), + ], + }); + + const app = getTestExpressApp(); + + await EmailPassword.signUp("public", "test@example.com", "password"); + + let res = await epSignIn(app, "test@example.com", "password"); + assert.equal("OK", res.body.status); + + res = await plessEmailSignInUp(app, "test@example.com", undefined); + assert.equal("DISALLOWED_FIRST_FACTOR_ERROR", res.body.status); + }); + + it("test that only a valid first factor is allowed to login and tenant config is prioritised", async function () { + const connectionURI = await startSTWithMultitenancy(); + SuperTokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + EmailPassword.init(), + Passwordless.init({ + contactMethod: "EMAIL", + flowType: "USER_INPUT_CODE", + }), + ThirdParty.init(), + Totp.init(), + MultiFactorAuth.init({ + firstFactors: ["emailpassword"], + }), + Session.init(), + ], + }); + + const app = getTestExpressApp(); + + await Multitenancy.createOrUpdateTenant("public", { + firstFactors: ["emailpassword", "otp-email"], + }); + + await EmailPassword.signUp("public", "test@example.com", "password"); + + let res = await epSignIn(app, "test@example.com", "password"); + assert.equal("OK", res.body.status); + + const code = await Passwordless.createCode({ + tenantId: "public", + email: "test@example.com", + }); + + res = await plessEmailSignInUp(app, "test@example.com"); + assert.equal("OK", res.body.status); + }); + + it("test that once user has more than one factor setup, they need 2FA to setup a new factor", async function () { + const connectionURI = await startSTWithMultitenancy(); + SuperTokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + EmailPassword.init(), + Passwordless.init({ + contactMethod: "EMAIL_OR_PHONE", + flowType: "USER_INPUT_CODE", + }), + ThirdParty.init(), + Totp.init(), + MultiFactorAuth.init(), + Session.init(), + ], + }); + + const app = getTestExpressApp(); + + await Multitenancy.createOrUpdateTenant("public", { + defaultRequiredFactorIds: ["otp-email", "otp-phone"], + }); + + await EmailPassword.signUp("public", "test@example.com", "password"); + + let res = await epSignIn(app, "test@example.com", "password"); + assert.equal("OK", res.body.status); + + let cookies = extractInfoFromResponse(res); + let accessToken = cookies.accessTokenFromAny; + + res = await plessEmailSignInUp(app, "test@example.com", accessToken); + assert.equal("OK", res.body.status); + assert.equal(true, res.body.user.isPrimaryUser); + assert.equal(2, res.body.user.loginMethods.length); + + // Try setting up totp without 2FA + res = await epSignIn(app, "test@example.com", "password"); + assert.equal("OK", res.body.status); + + cookies = extractInfoFromResponse(res); + accessToken = cookies.accessTokenFromAny; + + res = await plessPhoneSigninUp(app, "+919876543210", accessToken); + assert.equal("FACTOR_SETUP_NOT_ALLOWED_ERROR", res.body.status); + }); + + it("test that existing user sign in does not result in factor setup", async function () { + const connectionURI = await startSTWithMultitenancy(); + SuperTokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + EmailPassword.init(), + Passwordless.init({ + contactMethod: "EMAIL", + flowType: "USER_INPUT_CODE", + }), + ThirdParty.init(), + Totp.init(), + MultiFactorAuth.init({ + firstFactors: ["emailpassword"], + }), + Session.init(), + ], + }); + + const app = getTestExpressApp(); + + await Multitenancy.createOrUpdateTenant("public", { + firstFactors: ["emailpassword", "otp-email"], + }); + + await EmailPassword.signUp("public", "test1@example.com", "password"); + await EmailPassword.signUp("public", "test2@example.com", "password"); + + let res = await epSignIn(app, "test1@example.com", "password"); + assert.equal("OK", res.body.status); + + let cookies = extractInfoFromResponse(res); + let accessToken = cookies.accessTokenFromAny; + + res = await epSignIn(app, "test2@example.com", "password", accessToken); + assert.equal("OK", res.body.status); // older session should have been retained + + cookies = extractInfoFromResponse(res); + assert.equal(undefined, cookies.accessTokenFromAny); + }); + + it("test that a different primary user login retains old session", async function () { + const connectionURI = await startSTWithMultitenancy(); + SuperTokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + EmailPassword.init(), + Passwordless.init({ + contactMethod: "EMAIL", + flowType: "USER_INPUT_CODE", + }), + ThirdParty.init(), + Totp.init(), + MultiFactorAuth.init({ + firstFactors: ["emailpassword"], + }), + Session.init(), + ], + }); + + const app = getTestExpressApp(); + + const user1 = await EmailPassword.signUp("public", "test1@example.com", "password"); + const user2 = await Passwordless.signInUp({ + tenantId: "public", + email: "test1@example.com", + }); + await AccountLinking.createPrimaryUser(user1.recipeUserId); + await AccountLinking.linkAccounts(user2.recipeUserId, user1.user.id); + + const user3 = await EmailPassword.signUp("public", "test2@example.com", "password"); + const user4 = await Passwordless.signInUp({ + tenantId: "public", + email: "test2@example.com", + }); + await AccountLinking.createPrimaryUser(user3.recipeUserId); + await AccountLinking.linkAccounts(user4.recipeUserId, user3.user.id); + + res = await epSignIn(app, "test2@example.com", "password"); + assert.equal("OK", res.body.status); + + let cookies = extractInfoFromResponse(res); + const accessToken = cookies.accessTokenFromAny; + + res = await getMfaInfo(app, accessToken); + assert.equal("OK", res.body.status); + assert.equal("test2@example.com", res.body.email); + assert.deepEqual(["emailpassword", "otp-email"], res.body.factors.isAlreadySetup); + assert.deepEqual(["emailpassword", "otp-email", "thirdparty", "totp"], res.body.factors.isAllowedToSetup); + + res = await plessEmailSignInUp(app, "test1@example.com", accessToken); + assert.equal("OK", res.body.status); + cookies = extractInfoFromResponse(res); + assert.equal(undefined, cookies.accessTokenFromAny); + }); }); diff --git a/test/mfa/mfa.autoInit.test.js b/test/mfa/mfa.autoInit.test.js new file mode 100644 index 000000000..eaffd5b96 --- /dev/null +++ b/test/mfa/mfa.autoInit.test.js @@ -0,0 +1,59 @@ +const { printPath, setupST, startSTWithMultitenancy, killAllST, cleanST } = require("../utils"); +let assert = require("assert"); +const express = require("express"); +let { ProcessState } = require("../../lib/build/processState"); +let SuperTokens = require("../../"); +let { middleware, errorHandler } = require("../../framework/express"); +let MultiFactorAuth = require("../../lib/build/recipe/multifactorauth"); +let UserMetadata = require("../../lib/build/recipe/usermetadata"); +let Session = require("../../lib/build/recipe/session"); +let Totp = require("../../lib/build/recipe/totp"); +let EmailPassword = require("../../lib/build/recipe/emailpassword"); + +describe(`mfa-autoinit: ${printPath("[test/mfa/mfa.autoInit.test.js]")}`, function () { + beforeEach(async function () { + await killAllST(); + await setupST(); + ProcessState.getInstance().reset(); + }); + + after(async function () { + await killAllST(); + await cleanST(); + }); + + it("test usermetadata is auto-initialised if mfa is initialised", async function () { + const connectionURI = await startSTWithMultitenancy(); + SuperTokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [MultiFactorAuth.init(), Session.init()], + }); + + await UserMetadata.updateUserMetadata("test-userid", { key: "val" }); // should not have an error + }); + + it("test mfa is auto-initialized if totp is initialised", async function () { + const connectionURI = await startSTWithMultitenancy(); + SuperTokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [EmailPassword.init(), Totp.init(), Session.init()], + }); + + const user = await EmailPassword.signUp("public", "test@example.com", "password"); + await MultiFactorAuth.addToDefaultRequiredFactorsForUser(user.user.id, "totp"); // should not have an error + }); +}); diff --git a/test/mfa/mfa.claims.test.js b/test/mfa/mfa.claims.test.js deleted file mode 100644 index e69de29bb..000000000 diff --git a/test/mfa/mfa.recipeFunctions.test.js b/test/mfa/mfa.recipeFunctions.test.js new file mode 100644 index 000000000..9615c9ea4 --- /dev/null +++ b/test/mfa/mfa.recipeFunctions.test.js @@ -0,0 +1,300 @@ +const { printPath, setupST, startSTWithMultitenancy, killAllST, cleanST } = require("../utils"); +let assert = require("assert"); +const express = require("express"); +let { ProcessState } = require("../../lib/build/processState"); +let SuperTokens = require("../../"); +let { middleware, errorHandler } = require("../../framework/express"); +let MultiFactorAuth = require("../../lib/build/recipe/multifactorauth"); +let MultiFactorAuthRecipe = require("../../lib/build/recipe/multifactorauth/recipe").default; +let UserMetadata = require("../../lib/build/recipe/usermetadata"); +let Session = require("../../lib/build/recipe/session"); +let Totp = require("../../lib/build/recipe/totp"); +let EmailPassword = require("../../lib/build/recipe/emailpassword"); +let Passwordless = require("../../lib/build/recipe/passwordless"); +let ThirdParty = require("../../lib/build/recipe/thirdparty"); +let Multitenancy = require("../../lib/build/recipe/multitenancy"); +let AccountLinking = require("../../lib/build/recipe/accountlinking"); +const OTPAuth = require("otpauth"); + +describe(`mfa-recipeFunctions: ${printPath("[test/mfa/mfa.recipeFunctions.test.js]")}`, function () { + beforeEach(async function () { + await killAllST(); + await setupST(); + ProcessState.getInstance().reset(); + }); + + after(async function () { + await killAllST(); + await cleanST(); + }); + + it("test getFactorsSetupForUser with passwordless otp-email", async function () { + const connectionURI = await startSTWithMultitenancy(); + SuperTokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + EmailPassword.init(), + Passwordless.init({ + contactMethod: "EMAIL", + flowType: "USER_INPUT_CODE", + }), + ThirdParty.init(), + Totp.init(), + MultiFactorAuth.init(), + Session.init(), + ], + }); + + const user = await EmailPassword.signUp("public", "test@example.com", "password"); + let factorIds = await MultiFactorAuth.getFactorsSetupForUser("public", user.user.id); + assert.deepEqual(factorIds, ["emailpassword", "otp-email"]); + }); + + it("test getFactorsSetupForUser with passwordless otp-email and link-email", async function () { + const connectionURI = await startSTWithMultitenancy(); + SuperTokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + EmailPassword.init(), + Passwordless.init({ + contactMethod: "EMAIL", + flowType: "USER_INPUT_CODE_AND_MAGIC_LINK", + }), + ThirdParty.init(), + Totp.init(), + MultiFactorAuth.init(), + Session.init(), + ], + }); + + const user = await EmailPassword.signUp("public", "test@example.com", "password"); + let factorIds = await MultiFactorAuth.getFactorsSetupForUser("public", user.user.id); + assert.deepEqual(factorIds, ["emailpassword", "otp-email", "link-email"]); + }); + + it("test getFactorsSetupForUser with otp-phone", async function () { + const connectionURI = await startSTWithMultitenancy(); + SuperTokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + EmailPassword.init(), + Passwordless.init({ + contactMethod: "EMAIL_OR_PHONE", + flowType: "USER_INPUT_CODE", + }), + ThirdParty.init(), + Totp.init(), + MultiFactorAuth.init(), + Session.init(), + ], + }); + + const user = await Passwordless.signInUp({ + tenantId: "public", + phoneNumber: "+919876543210", + }); + let factorIds = await MultiFactorAuth.getFactorsSetupForUser("public", user.user.id); + assert.deepEqual(factorIds, ["otp-phone"]); + }); + + it("test getFactorsSetupForUser with totp", async function () { + const connectionURI = await startSTWithMultitenancy(); + SuperTokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + EmailPassword.init(), + Passwordless.init({ + contactMethod: "EMAIL_OR_PHONE", + flowType: "USER_INPUT_CODE", + }), + ThirdParty.init(), + Totp.init(), + MultiFactorAuth.init(), + Session.init(), + ], + }); + + const user = await Passwordless.signInUp({ + tenantId: "public", + phoneNumber: "+919876543210", + }); + const deviceRes = await Totp.createDevice(user.user.id); + const otp = new OTPAuth.TOTP({ + digits: 6, + period: 30, + secret: deviceRes.secret, + }).generate(); + await Totp.verifyDevice("public", user.user.id, deviceRes.deviceName, otp); + + let factorIds = await MultiFactorAuth.getFactorsSetupForUser("public", user.user.id); + assert.deepEqual(factorIds, ["otp-phone", "totp"]); + }); + + it("test getFactorsSetupForUser with totp but totp disabled in core", async function () { + const connectionURI = await startSTWithMultitenancy(); + SuperTokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + EmailPassword.init(), + Passwordless.init({ + contactMethod: "EMAIL_OR_PHONE", + flowType: "USER_INPUT_CODE", + }), + ThirdParty.init(), + Totp.init(), + MultiFactorAuth.init(), + Session.init(), + ], + }); + + const user = await Passwordless.signInUp({ + tenantId: "public", + phoneNumber: "+919876543210", + }); + const deviceRes = await Totp.createDevice(user.user.id); + const otp = new OTPAuth.TOTP({ + digits: 6, + period: 30, + secret: deviceRes.secret, + }).generate(); + await Totp.verifyDevice("public", user.user.id, deviceRes.deviceName, otp); + + await Multitenancy.createOrUpdateTenant("public", { totpEnabled: false }); + + let factorIds = await MultiFactorAuth.getFactorsSetupForUser("public", user.user.id); + assert.deepEqual(factorIds, ["otp-phone"]); + }); + + it("test getFactorsSetupForUser with linked accounts", async function () { + const connectionURI = await startSTWithMultitenancy(); + SuperTokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + EmailPassword.init(), + Passwordless.init({ + contactMethod: "EMAIL_OR_PHONE", + flowType: "USER_INPUT_CODE", + }), + ThirdParty.init(), + Totp.init(), + MultiFactorAuth.init(), + Session.init(), + ], + }); + + const user1 = await EmailPassword.signUp("public", "test@example.com", "password"); + const user2 = await Passwordless.signInUp({ + tenantId: "public", + phoneNumber: "+919876543210", + }); + await AccountLinking.createPrimaryUser(new SuperTokens.RecipeUserId(user1.user.id)); + await AccountLinking.linkAccounts(new SuperTokens.RecipeUserId(user2.user.id), user1.user.id); + + let factorIds = await MultiFactorAuth.getFactorsSetupForUser("public", user1.user.id); + assert.deepEqual(factorIds, ["emailpassword", "otp-email", "otp-phone"]); + }); + + it("test getMFARequirementsForAuth with passwordless otp-email", async function () { + const connectionURI = await startSTWithMultitenancy(); + SuperTokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + EmailPassword.init(), + Passwordless.init({ + contactMethod: "EMAIL", + flowType: "USER_INPUT_CODE", + }), + ThirdParty.init(), + Totp.init(), + MultiFactorAuth.init(), + Session.init(), + ], + }); + + const user = await EmailPassword.signUp("public", "test@example.com", "password"); + + const testCases = [ + { drfu: [], drft: [], c: {}, e: [] }, + { drfu: ["otp-phone"], drft: [], c: {}, e: ["otp-phone"] }, + { drfu: ["otp-phone", "otp-email"], drft: [], c: {}, e: ["otp-phone", "otp-email"] }, + { drfu: ["otp-phone", "otp-email"], drft: [], c: { "otp-email": 0 }, e: ["otp-phone"] }, + { drfu: ["otp-phone"], drft: ["otp-email"], c: {}, e: ["otp-phone", "otp-email"] }, + { + drfu: ["otp-phone", "otp-email", "totp"], + drft: [], + c: { "otp-phone": 0, "otp-email": 1 }, + e: ["otp-email", "totp"], + }, + ]; + + for (const tc of testCases) { + let requirements = await MultiFactorAuthRecipe.getInstanceOrThrowError().recipeInterfaceImpl.getMFARequirementsForAuth( + { + user: user.user, + defaultRequiredFactorIdsForUser: tc.drfu, + defaultRequiredFactorIdsForTenant: tc.drft, + completedFactors: tc.c, + tenantId: "public", + accessTokenPayload: {}, + factorsSetUpForUser: [], + userContext: {}, + } + ); + requirements = requirements[0].oneOf; + assert( + requirements.length === tc.e.length && + requirements.every((t) => tc.e.includes(t)) && + tc.e.every((t) => requirements.includes(t)) + ); + } + }); +}); diff --git a/test/mfa/mfa.withAccountLinking.test.js b/test/mfa/mfa.withAccountLinking.test.js new file mode 100644 index 000000000..720d5a609 --- /dev/null +++ b/test/mfa/mfa.withAccountLinking.test.js @@ -0,0 +1,738 @@ +const { + printPath, + setupST, + startSTWithMultitenancy, + killAllST, + cleanST, + extractInfoFromResponse, +} = require("../utils"); +let assert = require("assert"); +const express = require("express"); +let { ProcessState } = require("../../lib/build/processState"); +let SuperTokens = require("../../"); +let { middleware, errorHandler } = require("../../framework/express"); +let MultiFactorAuth = require("../../lib/build/recipe/multifactorauth"); +let MultiFactorAuthRecipe = require("../../lib/build/recipe/multifactorauth/recipe").default; +let UserMetadata = require("../../lib/build/recipe/usermetadata"); +let Session = require("../../lib/build/recipe/session"); +let Totp = require("../../lib/build/recipe/totp"); +let EmailPassword = require("../../lib/build/recipe/emailpassword"); +let Passwordless = require("../../lib/build/recipe/passwordless"); +let ThirdParty = require("../../lib/build/recipe/thirdparty"); +let Multitenancy = require("../../lib/build/recipe/multitenancy"); +let AccountLinking = require("../../lib/build/recipe/accountlinking"); +const OTPAuth = require("otpauth"); +const { json } = require("body-parser"); +const request = require("supertest"); +const { + epSignIn, + epSignUp, + plessEmailSignInUp, + plessPhoneSigninUp, + tpSignInUp, + getMfaInfo, + getTestExpressApp, +} = require("./utils"); + +describe(`mfa with account linking: ${printPath("[test/mfa/mfa.withAccountLinking.test.js]")}`, function () { + beforeEach(async function () { + await killAllST(); + await setupST(); + ProcessState.getInstance().reset(); + }); + + after(async function () { + await killAllST(); + await cleanST(); + }); + + it("test that thirdparty user sign up is rejected when another user with same email unverified exists", async function () { + const connectionURI = await startSTWithMultitenancy(); + SuperTokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + AccountLinking.init({ + shouldDoAutomaticAccountLinking: async () => ({ + shouldAutomaticallyLink: true, + shouldRequireVerification: true, + }), + }), + EmailPassword.init(), + Passwordless.init({ + contactMethod: "EMAIL", + flowType: "USER_INPUT_CODE", + }), + ThirdParty.init({ + signInAndUpFeature: { + providers: [ + { + config: { + thirdPartyId: "custom", + clients: [ + { + clientId: "clientid1", + }, + ], + }, + override: (oI) => { + oI.exchangeAuthCodeForOAuthTokens = async (input) => { + return input.redirectURIInfo.redirectURIQueryParams; + }; + oI.getUserInfo = async (input) => { + return { + thirdPartyUserId: input.oAuthTokens.email, + email: { + id: input.oAuthTokens.email, + isVerified: true, + }, + }; + }; + return oI; + }, + }, + ], + }, + }), + Totp.init(), + MultiFactorAuth.init(), + Session.init(), + ], + }); + + const app = getTestExpressApp(); + + let res = await epSignUp(app, "test1@example.com", "password1", undefined); + assert.equal("OK", res.body.status); + + let cookies = extractInfoFromResponse(res); + let accessToken = cookies.accessTokenFromAny; + + res = await plessPhoneSigninUp(app, "+919876543210", accessToken); + assert.equal("OK", res.body.status); + + res = await tpSignInUp(app, "custom", "test1@example.com", undefined); + assert.equal("SIGN_IN_UP_NOT_ALLOWED", res.body.status); + }); + + it("test factor setup with same email as another existing user when automatic account linking is turned off", async function () { + const connectionURI = await startSTWithMultitenancy(); + SuperTokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + EmailPassword.init(), + Passwordless.init({ + contactMethod: "EMAIL", + flowType: "USER_INPUT_CODE", + }), + ThirdParty.init({ + signInAndUpFeature: { + providers: [ + { + config: { + thirdPartyId: "custom", + clients: [ + { + clientId: "clientid1", + }, + ], + }, + override: (oI) => { + oI.exchangeAuthCodeForOAuthTokens = async (input) => { + return input.redirectURIInfo.redirectURIQueryParams; + }; + oI.getUserInfo = async (input) => { + return { + thirdPartyUserId: input.oAuthTokens.email, + email: { + id: input.oAuthTokens.email, + isVerified: true, + }, + }; + }; + return oI; + }, + }, + ], + }, + }), + Totp.init(), + MultiFactorAuth.init(), + Session.init(), + ], + }); + + const app = getTestExpressApp(); + + let res = await tpSignInUp(app, "custom", "test1@example.com", undefined); + assert.equal("OK", res.body.status); + + res = await epSignUp(app, "test2@example.com", "password1", undefined); + assert.equal("OK", res.body.status); + + let cookies = extractInfoFromResponse(res); + let accessToken = cookies.accessTokenFromAny; + + res = await plessEmailSignInUp(app, "test1@example.com", accessToken); + assert.equal("OK", res.body.status); + assert.equal(true, res.body.user.isPrimaryUser); + assert.equal(2, res.body.user.loginMethods.length); + assert.equal("emailpassword", res.body.user.loginMethods[0].recipeId); + assert.equal("passwordless", res.body.user.loginMethods[1].recipeId); + }); + + it("test factor setup with same email as another existing user when automatic account linking is turned on but verification not required", async function () { + const connectionURI = await startSTWithMultitenancy(); + SuperTokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + AccountLinking.init({ + shouldDoAutomaticAccountLinking: () => { + return { + shouldAutomaticallyLink: true, + shouldRequireVerification: false, + }; + }, + }), + EmailPassword.init(), + Passwordless.init({ + contactMethod: "EMAIL", + flowType: "USER_INPUT_CODE", + }), + ThirdParty.init({ + signInAndUpFeature: { + providers: [ + { + config: { + thirdPartyId: "custom", + clients: [ + { + clientId: "clientid1", + }, + ], + }, + override: (oI) => { + oI.exchangeAuthCodeForOAuthTokens = async (input) => { + return input.redirectURIInfo.redirectURIQueryParams; + }; + oI.getUserInfo = async (input) => { + return { + thirdPartyUserId: input.oAuthTokens.email, + email: { + id: input.oAuthTokens.email, + isVerified: true, + }, + }; + }; + return oI; + }, + }, + ], + }, + }), + Totp.init(), + MultiFactorAuth.init(), + Session.init(), + ], + }); + + const app = getTestExpressApp(); + + let res = await tpSignInUp(app, "custom", "test1@example.com", undefined); + assert.equal("OK", res.body.status); + + res = await epSignUp(app, "test2@example.com", "password1", undefined); + assert.equal("OK", res.body.status); + + let cookies = extractInfoFromResponse(res); + let accessToken = cookies.accessTokenFromAny; + + res = await plessEmailSignInUp(app, "test1@example.com", accessToken); + assert.equal("ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR", res.body.status); + + const usersRes = await SuperTokens.getUsersOldestFirst({ + tenantId: "public", + }); + + // we expect only 2 users because we should not have the user created by the factor setup, since the factor setup is expected to fail + assert.equal(2, usersRes.users.length); + }); + + it("test factor setup with same email as another existing user when automatic account linking is turned on and verification is required", async function () { + const connectionURI = await startSTWithMultitenancy(); + SuperTokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + AccountLinking.init({ + shouldDoAutomaticAccountLinking: () => { + return { + shouldAutomaticallyLink: true, + shouldRequireVerification: true, + }; + }, + }), + EmailPassword.init(), + Passwordless.init({ + contactMethod: "EMAIL", + flowType: "USER_INPUT_CODE", + }), + ThirdParty.init({ + signInAndUpFeature: { + providers: [ + { + config: { + thirdPartyId: "custom", + clients: [ + { + clientId: "clientid1", + }, + ], + }, + override: (oI) => { + oI.exchangeAuthCodeForOAuthTokens = async (input) => { + return input.redirectURIInfo.redirectURIQueryParams; + }; + oI.getUserInfo = async (input) => { + return { + thirdPartyUserId: input.oAuthTokens.email, + email: { + id: input.oAuthTokens.email, + isVerified: true, + }, + }; + }; + return oI; + }, + }, + ], + }, + }), + Totp.init(), + MultiFactorAuth.init(), + Session.init(), + ], + }); + + const app = getTestExpressApp(); + + let res = await tpSignInUp(app, "custom", "test1@example.com", undefined); + assert.equal("OK", res.body.status); + + res = await epSignUp(app, "test2@example.com", "password1", undefined); + assert.equal("OK", res.body.status); + + let cookies = extractInfoFromResponse(res); + let accessToken = cookies.accessTokenFromAny; + + res = await plessEmailSignInUp(app, "test1@example.com", accessToken); + assert.equal("ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR", res.body.status); + + const usersRes = await SuperTokens.getUsersOldestFirst({ + tenantId: "public", + }); + + // we expect only 2 users because we should not have the user created by the factor setup, since the factor setup is expected to fail + assert.equal(2, usersRes.users.length); + }); + + it("test factor setup with thirdparty same email as another existing user when automatic account linking is turned on and verification is required", async function () { + const connectionURI = await startSTWithMultitenancy(); + SuperTokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + AccountLinking.init({ + shouldDoAutomaticAccountLinking: () => { + return { + shouldAutomaticallyLink: true, + shouldRequireVerification: true, + }; + }, + }), + EmailPassword.init(), + Passwordless.init({ + contactMethod: "EMAIL", + flowType: "USER_INPUT_CODE", + }), + ThirdParty.init({ + signInAndUpFeature: { + providers: [ + { + config: { + thirdPartyId: "custom", + clients: [ + { + clientId: "clientid1", + }, + ], + }, + override: (oI) => { + oI.exchangeAuthCodeForOAuthTokens = async (input) => { + return input.redirectURIInfo.redirectURIQueryParams; + }; + oI.getUserInfo = async (input) => { + return { + thirdPartyUserId: input.oAuthTokens.email, + email: { + id: input.oAuthTokens.email, + isVerified: true, + }, + }; + }; + return oI; + }, + }, + { + config: { + thirdPartyId: "custom2", + clients: [ + { + clientId: "clientid1", + }, + ], + }, + override: (oI) => { + oI.exchangeAuthCodeForOAuthTokens = async (input) => { + return input.redirectURIInfo.redirectURIQueryParams; + }; + oI.getUserInfo = async (input) => { + return { + thirdPartyUserId: "custom2" + input.oAuthTokens.email, + email: { + id: input.oAuthTokens.email, + isVerified: true, + }, + }; + }; + return oI; + }, + }, + ], + }, + }), + Totp.init(), + MultiFactorAuth.init(), + Session.init(), + ], + }); + + const app = getTestExpressApp(); + + let res = await tpSignInUp(app, "custom", "test1@example.com", undefined); + assert.equal("OK", res.body.status); + + res = await epSignUp(app, "test2@example.com", "password1", undefined); + assert.equal("OK", res.body.status); + + let cookies = extractInfoFromResponse(res); + let accessToken = cookies.accessTokenFromAny; + + res = await tpSignInUp(app, "custom", "test2@example.com", accessToken); + assert.equal("OK", res.body.status); + + res = await tpSignInUp(app, "custom2", "test1@example.com", accessToken); + assert.equal("ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR", res.body.status); + + const usersRes = await SuperTokens.getUsersOldestFirst({ + tenantId: "public", + }); + + // we expect only 2 users because we should not have the user created by the factor setup, since the factor setup is expected to fail + assert.equal(2, usersRes.users.length); + }); + + it("test that unverified sign up is not allowed as a factor setup", async function () { + const connectionURI = await startSTWithMultitenancy(); + SuperTokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + AccountLinking.init({ + shouldDoAutomaticAccountLinking: async () => ({ + shouldAutomaticallyLink: true, + shouldRequireVerification: true, + }), + }), + EmailPassword.init(), + Passwordless.init({ + contactMethod: "EMAIL", + flowType: "USER_INPUT_CODE", + }), + ThirdParty.init({ + signInAndUpFeature: { + providers: [ + { + config: { + thirdPartyId: "custom", + clients: [ + { + clientId: "clientid1", + }, + ], + }, + override: (oI) => { + oI.exchangeAuthCodeForOAuthTokens = async (input) => { + return input.redirectURIInfo.redirectURIQueryParams; + }; + oI.getUserInfo = async (input) => { + return { + thirdPartyUserId: input.oAuthTokens.email, + email: { + id: input.oAuthTokens.email, + isVerified: true, + }, + }; + }; + return oI; + }, + }, + ], + }, + }), + Totp.init(), + MultiFactorAuth.init(), + Session.init(), + ], + }); + + const app = getTestExpressApp(); + + let res = await tpSignInUp(app, "custom", "test@example.com", undefined); + assert.equal("OK", res.body.status); + + let cookies = extractInfoFromResponse(res); + let accessToken = cookies.accessTokenFromAny; + + res = await epSignUp(app, "test2@example.com", "password1", accessToken); + assert.equal("FACTOR_SETUP_NOT_ALLOWED_ERROR", res.body.status); + }); + + it("test with account linking case 1", async function () { + // auto account linking is turned off + // Google signing up with e2 -> recipe user + // auto account linking is turned on + // Sign up with google with email e1 (with auto account linking on + verification required) + // Second factor is email password e2 -> should be rejected + // Google signing in with e2 -> should be allowed and become a primary user. + + let shouldAutomaticallyLink = false; + let shouldRequireVerification = false; + + const connectionURI = await startSTWithMultitenancy(); + SuperTokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + AccountLinking.init({ + shouldDoAutomaticAccountLinking: async () => ({ + shouldAutomaticallyLink, + shouldRequireVerification, + }), + }), + EmailPassword.init(), + Passwordless.init({ + contactMethod: "EMAIL", + flowType: "USER_INPUT_CODE", + }), + ThirdParty.init({ + signInAndUpFeature: { + providers: [ + { + config: { + thirdPartyId: "custom", + clients: [ + { + clientId: "clientid1", + }, + ], + }, + override: (oI) => { + oI.exchangeAuthCodeForOAuthTokens = async (input) => { + return input.redirectURIInfo.redirectURIQueryParams; + }; + oI.getUserInfo = async (input) => { + return { + thirdPartyUserId: input.oAuthTokens.email, + email: { + id: input.oAuthTokens.email, + isVerified: true, + }, + }; + }; + return oI; + }, + }, + ], + }, + }), + Totp.init(), + MultiFactorAuth.init({ + firstFactors: ["emailpassword", "thirdparty"], + }), + Session.init(), + ], + }); + + const app = getTestExpressApp(); + + let res = await tpSignInUp(app, "custom", "test2@example.com"); + assert.equal("OK", res.body.status); + + shouldAutomaticallyLink = true; + shouldRequireVerification = true; + + res = await tpSignInUp(app, "custom", "test1@example.com"); + assert.equal("OK", res.body.status); + + let cookies = extractInfoFromResponse(res); + let accessToken = cookies.accessTokenFromAny; + + res = await epSignUp(app, "test2@example.com", "password2", accessToken); + assert.equal("FACTOR_SETUP_NOT_ALLOWED_ERROR", res.body.status); + + res = await tpSignInUp(app, "custom", "test2@example.com", undefined); + assert.equal("OK", res.body.status); + + assert.equal(true, res.body.user.isPrimaryUser); + }); + + it("test with account linking case 2", async function () { + // auto account linking is turned off + // Google signing up with e2 -> recipe user + // auto account linking is turned on + // Sign up with google with email e1 (with auto account linking on + verification required) + // Second factor is email otp e2 -> should be linked + // Google signing in with e2 -> should link with google account with email e1 + + let shouldAutomaticallyLink = false; + let shouldRequireVerification = false; + + const connectionURI = await startSTWithMultitenancy(); + SuperTokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [ + AccountLinking.init({ + shouldDoAutomaticAccountLinking: async () => ({ + shouldAutomaticallyLink, + shouldRequireVerification, + }), + }), + EmailPassword.init(), + Passwordless.init({ + contactMethod: "EMAIL", + flowType: "USER_INPUT_CODE", + }), + ThirdParty.init({ + signInAndUpFeature: { + providers: [ + { + config: { + thirdPartyId: "custom", + clients: [ + { + clientId: "clientid1", + }, + ], + }, + override: (oI) => { + oI.exchangeAuthCodeForOAuthTokens = async (input) => { + return input.redirectURIInfo.redirectURIQueryParams; + }; + oI.getUserInfo = async (input) => { + return { + thirdPartyUserId: input.oAuthTokens.email, + email: { + id: input.oAuthTokens.email, + isVerified: true, + }, + }; + }; + return oI; + }, + }, + ], + }, + }), + Totp.init(), + MultiFactorAuth.init({ + firstFactors: ["emailpassword", "thirdparty"], + }), + Session.init(), + ], + }); + + const app = getTestExpressApp(); + + let res = await tpSignInUp(app, "custom", "test2@example.com"); + assert.equal("OK", res.body.status); + + shouldAutomaticallyLink = true; + shouldRequireVerification = true; + + res = await tpSignInUp(app, "custom", "test1@example.com"); + assert.equal("OK", res.body.status); + + let cookies = extractInfoFromResponse(res); + let accessToken = cookies.accessTokenFromAny; + + res = await plessEmailSignInUp(app, "test2@example.com", accessToken); + assert.equal("OK", res.body.status); + + res = await tpSignInUp(app, "custom", "test2@example.com", undefined); + assert.equal("OK", res.body.status); + assert.equal(true, res.body.user.isPrimaryUser); + assert.equal(3, res.body.user.loginMethods.length); + }); +}); diff --git a/test/mfa/utils.js b/test/mfa/utils.js new file mode 100644 index 000000000..9daca1a40 --- /dev/null +++ b/test/mfa/utils.js @@ -0,0 +1,280 @@ +const express = require("express"); +let { middleware, errorHandler } = require("../../framework/express"); +let Passwordless = require("../../lib/build/recipe/passwordless"); +const { json } = require("body-parser"); +const request = require("supertest"); + +module.exports.getTestExpressApp = function () { + const app = express(); + + app.use(middleware()); + app.use(json()); + + app.use(errorHandler()); + return app; +}; + +module.exports.epSignUp = async function (app, email, password, accessToken) { + if (accessToken === undefined) { + return await new Promise((resolve) => + request(app) + .post("/auth/signup") + .send({ + formFields: [ + { + id: "password", + value: password, + }, + { + id: "email", + value: email, + }, + ], + }) + .expect(200) + .end((err, res) => { + if (err) { + resolve(undefined); + } else { + resolve(res); + } + }) + ); + } else { + return await new Promise((resolve) => + request(app) + .post("/auth/signup") + .set("Authorization", `Bearer ${accessToken}`) + .send({ + formFields: [ + { + id: "password", + value: password, + }, + { + id: "email", + value: email, + }, + ], + }) + .expect(200) + .end((err, res) => { + if (err) { + resolve(undefined); + } else { + resolve(res); + } + }) + ); + } +}; + +module.exports.epSignIn = async function (app, email, password, accessToken) { + if (accessToken === undefined) { + return await new Promise((resolve) => + request(app) + .post("/auth/signin") + .send({ + formFields: [ + { + id: "password", + value: password, + }, + { + id: "email", + value: email, + }, + ], + }) + .expect(200) + .end((err, res) => { + if (err) { + resolve(undefined); + } else { + resolve(res); + } + }) + ); + } else { + return await new Promise((resolve) => + request(app) + .post("/auth/signin") + .set("Authorization", `Bearer ${accessToken}`) + .send({ + formFields: [ + { + id: "password", + value: password, + }, + { + id: "email", + value: email, + }, + ], + }) + .expect(200) + .end((err, res) => { + if (err) { + resolve(undefined); + } else { + resolve(res); + } + }) + ); + } +}; + +module.exports.plessEmailSignInUp = async function (app, email, accessToken) { + const code = await Passwordless.createCode({ + tenantId: "public", + email, + }); + + if (accessToken === undefined) { + return await new Promise((resolve, reject) => + request(app) + .post("/auth/signinup/code/consume") + .send({ + preAuthSessionId: code.preAuthSessionId, + userInputCode: code.userInputCode, + deviceId: code.deviceId, + }) + .expect(200) + .end((err, res) => { + if (err) { + reject(err); + } else { + resolve(res); + } + }) + ); + } else { + return await new Promise((resolve, reject) => + request(app) + .post("/auth/signinup/code/consume") + .set("Authorization", `Bearer ${accessToken}`) + .send({ + preAuthSessionId: code.preAuthSessionId, + userInputCode: code.userInputCode, + deviceId: code.deviceId, + }) + .expect(200) + .end((err, res) => { + if (err) { + reject(err); + } else { + resolve(res); + } + }) + ); + } +}; + +module.exports.plessPhoneSigninUp = async function (app, phoneNumber, accessToken) { + const code = await Passwordless.createCode({ + tenantId: "public", + phoneNumber, + }); + + if (accessToken === undefined) { + return await new Promise((resolve, reject) => + request(app) + .post("/auth/signinup/code/consume") + .send({ + preAuthSessionId: code.preAuthSessionId, + userInputCode: code.userInputCode, + deviceId: code.deviceId, + }) + .expect(200) + .end((err, res) => { + if (err) { + reject(err); + } else { + resolve(res); + } + }) + ); + } else { + return await new Promise((resolve, reject) => + request(app) + .post("/auth/signinup/code/consume") + .set("Authorization", `Bearer ${accessToken}`) + .send({ + preAuthSessionId: code.preAuthSessionId, + userInputCode: code.userInputCode, + deviceId: code.deviceId, + }) + .expect(200) + .end((err, res) => { + if (err) { + reject(err); + } else { + resolve(res); + } + }) + ); + } +}; + +module.exports.tpSignInUp = async function (app, thirdPartyId, email, accessToken) { + if (accessToken === undefined) { + return await new Promise((resolve) => + request(app) + .post("/auth/signinup") + .send({ + thirdPartyId: thirdPartyId, + redirectURIInfo: { + redirectURIOnProviderDashboard: "http://127.0.0.1/callback", + redirectURIQueryParams: { + email: email, + }, + }, + }) + .end((err, res) => { + if (err) { + resolve(undefined); + } else { + resolve(res); + } + }) + ); + } else { + return await new Promise((resolve) => + request(app) + .post("/auth/signinup") + .set("Authorization", `Bearer ${accessToken}`) + .send({ + thirdPartyId: thirdPartyId, + redirectURIInfo: { + redirectURIOnProviderDashboard: "http://127.0.0.1/callback", + redirectURIQueryParams: { + email: email, + }, + }, + }) + .end((err, res) => { + if (err) { + resolve(undefined); + } else { + resolve(res); + } + }) + ); + } +}; + +module.exports.getMfaInfo = async function (app, accessToken) { + return await new Promise((resolve) => + request(app) + .get("/auth/mfa/info") + .set("Authorization", `Bearer ${accessToken}`) + .expect(200) + .end((err, res) => { + if (err) { + resolve(undefined); + } else { + resolve(res); + } + }) + ); +}; diff --git a/test/querier.test.js b/test/querier.test.js index eda9b9369..43873146e 100644 --- a/test/querier.test.js +++ b/test/querier.test.js @@ -96,7 +96,7 @@ describe(`Querier: ${printPath("[test/querier.test.js]")}`, function () { return this.req.headers; }); - let response = await querier.sendGetRequest(new NormalisedURLPath("/recipe"), {}); + let response = await querier.sendGetRequest(new NormalisedURLPath("/recipe"), {}, {}); assert.deepStrictEqual(response.rid, ["session"]); nock(connectionURI, { @@ -107,7 +107,7 @@ describe(`Querier: ${printPath("[test/querier.test.js]")}`, function () { return this.req.headers; }); - let response2 = await querier.sendGetRequest(new NormalisedURLPath("/recipe/random"), {}); + let response2 = await querier.sendGetRequest(new NormalisedURLPath("/recipe/random"), {}, {}); assert.deepStrictEqual(response2.rid, ["session"]); nock(connectionURI, { @@ -118,7 +118,7 @@ describe(`Querier: ${printPath("[test/querier.test.js]")}`, function () { return this.req.headers; }); - let response3 = await querier.sendGetRequest(new NormalisedURLPath("/test"), {}); + let response3 = await querier.sendGetRequest(new NormalisedURLPath("/test"), {}, {}); assert.strictEqual(response3.rid, undefined); }); @@ -136,7 +136,7 @@ describe(`Querier: ${printPath("[test/querier.test.js]")}`, function () { }); try { let q = Querier.getNewInstanceOrThrowError(undefined); - await q.sendGetRequest(new NormalisedURLPath("", "/"), {}); + await q.sendGetRequest(new NormalisedURLPath("", "/"), {}, {}); throw new Error(); } catch (err) { if (err.message !== "No SuperTokens core available to query") { @@ -162,11 +162,11 @@ describe(`Querier: ${printPath("[test/querier.test.js]")}`, function () { recipeList: [Session.init({ getTokenTransferMethod: () => "cookie", antiCsrf: "VIA_TOKEN" })], }); let q = Querier.getNewInstanceOrThrowError(undefined); - assert.equal(await q.sendGetRequest(new NormalisedURLPath("/hello"), {}), "Hello\n"); - assert.equal(await q.sendDeleteRequest(new NormalisedURLPath("/hello"), {}), "Hello\n"); + assert.equal(await q.sendGetRequest(new NormalisedURLPath("/hello"), {}, {}), "Hello\n"); + assert.equal(await q.sendDeleteRequest(new NormalisedURLPath("/hello"), {}, undefined, {}), "Hello\n"); let hostsAlive = q.getHostsAliveForTesting(); assert.equal(hostsAlive.size, 3); - assert.equal(await q.sendGetRequest(new NormalisedURLPath("/hello"), {}), "Hello\n"); // this will be the 4th API call + assert.equal(await q.sendGetRequest(new NormalisedURLPath("/hello"), {}, {}), "Hello\n"); // this will be the 4th API call hostsAlive = q.getHostsAliveForTesting(); assert.equal(hostsAlive.size, 3); assert.equal(hostsAlive.has(connectionURI), true); @@ -189,11 +189,11 @@ describe(`Querier: ${printPath("[test/querier.test.js]")}`, function () { recipeList: [Session.init({ getTokenTransferMethod: () => "cookie", antiCsrf: "VIA_TOKEN" })], }); let q = Querier.getNewInstanceOrThrowError(undefined); - assert.equal(await q.sendGetRequest(new NormalisedURLPath("/hello"), {}), "Hello\n"); - assert.equal(await q.sendPostRequest(new NormalisedURLPath("/hello"), {}), "Hello\n"); + assert.equal(await q.sendGetRequest(new NormalisedURLPath("/hello"), {}, {}), "Hello\n"); + assert.equal(await q.sendPostRequest(new NormalisedURLPath("/hello"), {}, {}), "Hello\n"); let hostsAlive = q.getHostsAliveForTesting(); assert.equal(hostsAlive.size, 2); - assert.equal(await q.sendPutRequest(new NormalisedURLPath("/hello"), {}), "Hello\n"); // this will be the 4th API call + assert.equal(await q.sendPutRequest(new NormalisedURLPath("/hello"), {}, {}), "Hello\n"); // this will be the 4th API call hostsAlive = q.getHostsAliveForTesting(); assert.equal(hostsAlive.size, 2); assert.equal(hostsAlive.has(connectionURI), true); diff --git a/test/ratelimiting.test.js b/test/ratelimiting.test.js index c909944d5..40941a13b 100644 --- a/test/ratelimiting.test.js +++ b/test/ratelimiting.test.js @@ -85,7 +85,7 @@ describe(`Querier rate limiting: ${printPath("[test/ratelimiting.test.js]")}`, ( let q = Querier.getNewInstanceOrThrowError(undefined); try { - await q.sendGetRequest(new NormalisedURLPath("/testing"), {}); + await q.sendGetRequest(new NormalisedURLPath("/testing"), {}, {}); } catch (e) { if (!e.message.includes("with status code: 429")) { throw e; @@ -95,10 +95,10 @@ describe(`Querier rate limiting: ${printPath("[test/ratelimiting.test.js]")}`, ( // 1 initial request + 5 retries assert.equal(numbersOfTimesRetried, 6); - await q.sendGetRequest(new NormalisedURLPath("/testing2"), {}); + await q.sendGetRequest(new NormalisedURLPath("/testing2"), {}, {}); assert.equal(numberOfTimesSecondCalled, 3); - await q.sendGetRequest(new NormalisedURLPath("/testing3"), {}); + await q.sendGetRequest(new NormalisedURLPath("/testing3"), {}, {}); assert.equal(numberOfTimesThirdCalled, 1); server.close(); @@ -146,7 +146,7 @@ describe(`Querier rate limiting: ${printPath("[test/ratelimiting.test.js]")}`, ( let q = Querier.getNewInstanceOrThrowError(undefined); try { - await q.sendGetRequest(new NormalisedURLPath("/testing"), {}); + await q.sendGetRequest(new NormalisedURLPath("/testing"), {}, {}); } catch (e) { if (!e.message.includes("with status code: 429")) { throw e; @@ -207,7 +207,7 @@ describe(`Querier rate limiting: ${printPath("[test/ratelimiting.test.js]")}`, ( const callApi1 = async () => { try { - await q.sendGetRequest(new NormalisedURLPath("/testing"), { id: "1" }); + await q.sendGetRequest(new NormalisedURLPath("/testing"), { id: "1" }, {}); } catch (e) { if (!e.message.includes("with status code: 429")) { throw e; @@ -217,7 +217,7 @@ describe(`Querier rate limiting: ${printPath("[test/ratelimiting.test.js]")}`, ( const callApi2 = async () => { try { - await q.sendGetRequest(new NormalisedURLPath("/testing"), { id: "2" }); + await q.sendGetRequest(new NormalisedURLPath("/testing"), { id: "2" }, {}); } catch (e) { if (!e.message.includes("with status code: 429")) { throw e; diff --git a/test/session.test.js b/test/session.test.js index f19ca2e13..d564174c6 100644 --- a/test/session.test.js +++ b/test/session.test.js @@ -315,6 +315,7 @@ describe(`session: ${printPath("[test/session.test.js]")}`, function () { new RecipeUserId(""), false, {}, + {}, {} ); @@ -322,7 +323,8 @@ describe(`session: ${printPath("[test/session.test.js]")}`, function () { SessionRecipe.getInstanceOrThrowError().recipeInterfaceImpl.helpers, response.refreshToken.token, response.antiCsrfToken, - false + false, + {} ); await SessionFunctions.getSession( @@ -330,7 +332,8 @@ describe(`session: ${printPath("[test/session.test.js]")}`, function () { parseJWTWithoutSignatureVerification(response2.accessToken.token), response2.antiCsrfToken, true, - false + false, + {} ); try { @@ -338,7 +341,8 @@ describe(`session: ${printPath("[test/session.test.js]")}`, function () { SessionRecipe.getInstanceOrThrowError().recipeInterfaceImpl.helpers, response.refreshToken.token, response.antiCsrfToken, - false + false, + {} ); throw new Error("should not have come here"); } catch (err) { @@ -369,6 +373,7 @@ describe(`session: ${printPath("[test/session.test.js]")}`, function () { new RecipeUserId(""), false, {}, + {}, {} ); @@ -376,7 +381,8 @@ describe(`session: ${printPath("[test/session.test.js]")}`, function () { SessionRecipe.getInstanceOrThrowError().recipeInterfaceImpl.helpers, response.refreshToken.token, response.antiCsrfToken, - false + false, + {} ); await SessionFunctions.getSession( @@ -384,7 +390,8 @@ describe(`session: ${printPath("[test/session.test.js]")}`, function () { parseJWTWithoutSignatureVerification(response2.accessToken.token), response2.antiCsrfToken, true, - false + false, + {} ); try { @@ -392,7 +399,8 @@ describe(`session: ${printPath("[test/session.test.js]")}`, function () { SessionRecipe.getInstanceOrThrowError().recipeInterfaceImpl.helpers, response.refreshToken.token, response.antiCsrfToken, - false + false, + {} ); throw new Error("should not have come here"); } catch (err) { @@ -452,6 +460,7 @@ describe(`session: ${printPath("[test/session.test.js]")}`, function () { new RecipeUserId(""), false, {}, + {}, {} ); assert.notEqual(response.session, undefined); @@ -465,7 +474,8 @@ describe(`session: ${printPath("[test/session.test.js]")}`, function () { parseJWTWithoutSignatureVerification(response.accessToken.token), response.antiCsrfToken, true, - false + false, + {} ); let verifyState3 = await ProcessState.getInstance().waitForEvent(PROCESS_STATE.CALLING_SERVICE_IN_VERIFY, 1500); assert.strictEqual(verifyState3, undefined); @@ -474,7 +484,8 @@ describe(`session: ${printPath("[test/session.test.js]")}`, function () { s.recipeInterfaceImpl.helpers, response.refreshToken.token, response.antiCsrfToken, - false + false, + {} ); assert(response2.session !== undefined); assert(response2.accessToken !== undefined); @@ -487,7 +498,8 @@ describe(`session: ${printPath("[test/session.test.js]")}`, function () { parseJWTWithoutSignatureVerification(response2.accessToken.token), response2.antiCsrfToken, true, - false + false, + {} ); let verifyState = await ProcessState.getInstance().waitForEvent(PROCESS_STATE.CALLING_SERVICE_IN_VERIFY); assert(verifyState !== undefined); @@ -502,7 +514,8 @@ describe(`session: ${printPath("[test/session.test.js]")}`, function () { parseJWTWithoutSignatureVerification(response3.accessToken.token), response2.antiCsrfToken, true, - false + false, + {} ); let verifyState2 = await ProcessState.getInstance().waitForEvent(PROCESS_STATE.CALLING_SERVICE_IN_VERIFY, 1000); assert(verifyState2 === undefined); @@ -510,7 +523,11 @@ describe(`session: ${printPath("[test/session.test.js]")}`, function () { assert(response4.accessToken === undefined); assert(Object.keys(response4).length === 1); - let response5 = await SessionFunctions.revokeSession(s.recipeInterfaceImpl.helpers, response4.session.handle); + let response5 = await SessionFunctions.revokeSession( + s.recipeInterfaceImpl.helpers, + response4.session.handle, + {} + ); assert(response5 === true); }); @@ -537,6 +554,7 @@ describe(`session: ${printPath("[test/session.test.js]")}`, function () { new RecipeUserId(""), false, {}, + {}, {} ); @@ -545,7 +563,8 @@ describe(`session: ${printPath("[test/session.test.js]")}`, function () { parseJWTWithoutSignatureVerification(response.accessToken.token), response.antiCsrfToken, true, - true + true, + {} ); assert(response2.session != undefined); assert.strictEqual(Object.keys(response2.session).length, 6); @@ -555,7 +574,8 @@ describe(`session: ${printPath("[test/session.test.js]")}`, function () { parseJWTWithoutSignatureVerification(response.accessToken.token), response.antiCsrfToken, false, - true + true, + {} ); assert(response3.session != undefined); assert.strictEqual(Object.keys(response3.session).length, 6); @@ -584,6 +604,7 @@ describe(`session: ${printPath("[test/session.test.js]")}`, function () { new RecipeUserId(""), false, {}, + {}, {} ); @@ -593,7 +614,8 @@ describe(`session: ${printPath("[test/session.test.js]")}`, function () { parseJWTWithoutSignatureVerification(response.accessToken.token), undefined, false, - false + false, + {} ); assert.notStrictEqual(response2.session, undefined); @@ -606,7 +628,8 @@ describe(`session: ${printPath("[test/session.test.js]")}`, function () { parseJWTWithoutSignatureVerification(response.accessToken.token), undefined, true, - false + false, + {} ); throw new Error("should not have come here"); } catch (err) { @@ -639,12 +662,20 @@ describe(`session: ${printPath("[test/session.test.js]")}`, function () { new RecipeUserId("someUniqueUserId"), false, {}, + {}, {} ); - let res2 = await SessionFunctions.revokeSession(s.helpers, res.session.handle); + let res2 = await SessionFunctions.revokeSession(s.helpers, res.session.handle, {}); assert(res2 === true); - let res3 = await SessionFunctions.getAllSessionHandlesForUser(s.helpers, "someUniqueUserId", true); + let res3 = await SessionFunctions.getAllSessionHandlesForUser( + s.helpers, + "someUniqueUserId", + true, + undefined, + undefined, + {} + ); assert(res3.length === 0); //create multiple sessions with the same userID and use revokeAllSessionsForUser to revoke sessions @@ -654,6 +685,7 @@ describe(`session: ${printPath("[test/session.test.js]")}`, function () { new RecipeUserId("someUniqueUserId"), false, {}, + {}, {} ); await SessionFunctions.createNewSession( @@ -662,24 +694,53 @@ describe(`session: ${printPath("[test/session.test.js]")}`, function () { new RecipeUserId("someUniqueUserId"), false, {}, + {}, {} ); - let sessionIdResponse = await SessionFunctions.getAllSessionHandlesForUser(s.helpers, "someUniqueUserId"); + let sessionIdResponse = await SessionFunctions.getAllSessionHandlesForUser( + s.helpers, + "someUniqueUserId", + undefined, + undefined, + undefined, + {} + ); assert(sessionIdResponse.length === 2); - let response = await SessionFunctions.revokeAllSessionsForUser(s.helpers, "someUniqueUserId"); + let response = await SessionFunctions.revokeAllSessionsForUser( + s.helpers, + "someUniqueUserId", + undefined, + undefined, + undefined, + {} + ); assert(response.length === 2); - sessionIdResponse = await SessionFunctions.getAllSessionHandlesForUser(s.helpers, "someUniqueUserId"); + sessionIdResponse = await SessionFunctions.getAllSessionHandlesForUser( + s.helpers, + "someUniqueUserId", + undefined, + undefined, + undefined, + {} + ); assert(sessionIdResponse.length === 0); //revoke a session with a session handle that does not exist - let resp = await SessionFunctions.revokeSession(s.helpers, ""); + let resp = await SessionFunctions.revokeSession(s.helpers, "", {}); assert(resp === false); //revoke a session with a userId that does not exist - let resp2 = await SessionFunctions.revokeAllSessionsForUser(s.helpers, "random"); + let resp2 = await SessionFunctions.revokeAllSessionsForUser( + s.helpers, + "random", + undefined, + undefined, + undefined, + {} + ); assert(resp2.length === 0); }); @@ -700,20 +761,22 @@ describe(`session: ${printPath("[test/session.test.js]")}`, function () { let s = SessionRecipe.getInstanceOrThrowError().recipeInterfaceImpl; //adding session data - let res = await SessionFunctions.createNewSession(s.helpers, "public", new RecipeUserId(""), false, {}, {}); - await SessionFunctions.updateSessionDataInDatabase(s.helpers, res.session.handle, { key: "value" }); + let res = await SessionFunctions.createNewSession(s.helpers, "public", new RecipeUserId(""), false, {}, {}, {}); + await SessionFunctions.updateSessionDataInDatabase(s.helpers, res.session.handle, { key: "value" }, {}); - let res2 = (await SessionFunctions.getSessionInformation(s.helpers, res.session.handle)).sessionDataInDatabase; + let res2 = (await SessionFunctions.getSessionInformation(s.helpers, res.session.handle, {})) + .sessionDataInDatabase; assert.deepStrictEqual(res2, { key: "value" }); //changing the value of session data with the same key - await SessionFunctions.updateSessionDataInDatabase(s.helpers, res.session.handle, { key: "value 2" }); + await SessionFunctions.updateSessionDataInDatabase(s.helpers, res.session.handle, { key: "value 2" }, {}); - let res3 = (await SessionFunctions.getSessionInformation(s.helpers, res.session.handle)).sessionDataInDatabase; + let res3 = (await SessionFunctions.getSessionInformation(s.helpers, res.session.handle, {})) + .sessionDataInDatabase; assert.deepStrictEqual(res3, { key: "value 2" }); //passing invalid session handle when updating session data - assert(!(await SessionFunctions.updateSessionDataInDatabase(s.helpers, "random", { key2: "value2" }))); + assert(!(await SessionFunctions.updateSessionDataInDatabase(s.helpers, "random", { key2: "value2" }, {}))); }); it("test manipulating session data with new get session function", async function () { @@ -740,20 +803,20 @@ describe(`session: ${printPath("[test/session.test.js]")}`, function () { let s = SessionRecipe.getInstanceOrThrowError().recipeInterfaceImpl; //adding session data - let res = await SessionFunctions.createNewSession(s.helpers, "public", new RecipeUserId(""), false, {}, {}); - await SessionFunctions.updateSessionDataInDatabase(s.helpers, res.session.handle, { key: "value" }); + let res = await SessionFunctions.createNewSession(s.helpers, "public", new RecipeUserId(""), false, {}, {}, {}); + await SessionFunctions.updateSessionDataInDatabase(s.helpers, res.session.handle, { key: "value" }, {}); - let res2 = await SessionFunctions.getSessionInformation(s.helpers, res.session.handle); - assert.deepStrictEqual(res2.sessionDataInDatabase, { key: "value" }); + let res2 = await SessionFunctions.getSessionInformation(s.helpers, res.session.handle, {}); + assert.deepStrictEqual(res2.sessionDataInDatabase, { key: "value" }, {}); //changing the value of session data with the same key - await SessionFunctions.updateSessionDataInDatabase(s.helpers, res.session.handle, { key: "value 2" }); + await SessionFunctions.updateSessionDataInDatabase(s.helpers, res.session.handle, { key: "value 2" }, {}); - let res3 = await SessionFunctions.getSessionInformation(s.helpers, res.session.handle); - assert.deepStrictEqual(res3.sessionDataInDatabase, { key: "value 2" }); + let res3 = await SessionFunctions.getSessionInformation(s.helpers, res.session.handle, {}); + assert.deepStrictEqual(res3.sessionDataInDatabase, { key: "value 2" }, {}); //passing invalid session handle when updating session data - assert(!(await SessionFunctions.updateSessionDataInDatabase(s.helpers, "random", { key2: "value2" }))); + assert(!(await SessionFunctions.updateSessionDataInDatabase(s.helpers, "random", { key2: "value2" }, {}))); }); it("test null and undefined values passed for session data", async function () { @@ -772,29 +835,42 @@ describe(`session: ${printPath("[test/session.test.js]")}`, function () { let s = SessionRecipe.getInstanceOrThrowError().recipeInterfaceImpl; //adding session data - let res = await SessionFunctions.createNewSession(s.helpers, "public", new RecipeUserId(""), false, {}, null); + let res = await SessionFunctions.createNewSession( + s.helpers, + "public", + new RecipeUserId(""), + false, + {}, + null, + {} + ); - let res2 = (await SessionFunctions.getSessionInformation(s.helpers, res.session.handle)).sessionDataInDatabase; + let res2 = (await SessionFunctions.getSessionInformation(s.helpers, res.session.handle, {})) + .sessionDataInDatabase; assert.deepStrictEqual(res2, {}); - await SessionFunctions.updateSessionDataInDatabase(s.helpers, res.session.handle, { key: "value" }); + await SessionFunctions.updateSessionDataInDatabase(s.helpers, res.session.handle, { key: "value" }, {}); - let res3 = (await SessionFunctions.getSessionInformation(s.helpers, res.session.handle)).sessionDataInDatabase; + let res3 = (await SessionFunctions.getSessionInformation(s.helpers, res.session.handle, {})) + .sessionDataInDatabase; assert.deepStrictEqual(res3, { key: "value" }); - await SessionFunctions.updateSessionDataInDatabase(s.helpers, res.session.handle, undefined); + await SessionFunctions.updateSessionDataInDatabase(s.helpers, res.session.handle, undefined, {}); - let res4 = (await SessionFunctions.getSessionInformation(s.helpers, res.session.handle)).sessionDataInDatabase; + let res4 = (await SessionFunctions.getSessionInformation(s.helpers, res.session.handle, {})) + .sessionDataInDatabase; assert.deepStrictEqual(res4, {}); - await SessionFunctions.updateSessionDataInDatabase(s.helpers, res.session.handle, { key: "value 2" }); + await SessionFunctions.updateSessionDataInDatabase(s.helpers, res.session.handle, { key: "value 2" }, {}); - let res5 = (await SessionFunctions.getSessionInformation(s.helpers, res.session.handle)).sessionDataInDatabase; + let res5 = (await SessionFunctions.getSessionInformation(s.helpers, res.session.handle, {})) + .sessionDataInDatabase; assert.deepStrictEqual(res5, { key: "value 2" }); - await SessionFunctions.updateSessionDataInDatabase(s.helpers, res.session.handle, null); + await SessionFunctions.updateSessionDataInDatabase(s.helpers, res.session.handle, null, {}); - let res6 = (await SessionFunctions.getSessionInformation(s.helpers, res.session.handle)).sessionDataInDatabase; + let res6 = (await SessionFunctions.getSessionInformation(s.helpers, res.session.handle, {})) + .sessionDataInDatabase; assert.deepStrictEqual(res6, {}); }); @@ -822,29 +898,37 @@ describe(`session: ${printPath("[test/session.test.js]")}`, function () { let s = SessionRecipe.getInstanceOrThrowError().recipeInterfaceImpl; //adding session data - let res = await SessionFunctions.createNewSession(s.helpers, "public", new RecipeUserId(""), false, {}, null); + let res = await SessionFunctions.createNewSession( + s.helpers, + "public", + new RecipeUserId(""), + false, + {}, + null, + {} + ); - let res2 = await SessionFunctions.getSessionInformation(s.helpers, res.session.handle); + let res2 = await SessionFunctions.getSessionInformation(s.helpers, res.session.handle, {}); assert.deepStrictEqual(res2.sessionDataInDatabase, {}); - await SessionFunctions.updateSessionDataInDatabase(s.helpers, res.session.handle, { key: "value" }); + await SessionFunctions.updateSessionDataInDatabase(s.helpers, res.session.handle, { key: "value" }, {}); - let res3 = await SessionFunctions.getSessionInformation(s.helpers, res.session.handle); + let res3 = await SessionFunctions.getSessionInformation(s.helpers, res.session.handle, {}); assert.deepStrictEqual(res3.sessionDataInDatabase, { key: "value" }); - await SessionFunctions.updateSessionDataInDatabase(s.helpers, res.session.handle, undefined); + await SessionFunctions.updateSessionDataInDatabase(s.helpers, res.session.handle, undefined, {}); - let res4 = await SessionFunctions.getSessionInformation(s.helpers, res.session.handle); + let res4 = await SessionFunctions.getSessionInformation(s.helpers, res.session.handle, {}); assert.deepStrictEqual(res4.sessionDataInDatabase, {}); - await SessionFunctions.updateSessionDataInDatabase(s.helpers, res.session.handle, { key: "value 2" }); + await SessionFunctions.updateSessionDataInDatabase(s.helpers, res.session.handle, { key: "value 2" }, {}); - let res5 = await SessionFunctions.getSessionInformation(s.helpers, res.session.handle); + let res5 = await SessionFunctions.getSessionInformation(s.helpers, res.session.handle, {}); assert.deepStrictEqual(res5.sessionDataInDatabase, { key: "value 2" }); - await SessionFunctions.updateSessionDataInDatabase(s.helpers, res.session.handle, null); + await SessionFunctions.updateSessionDataInDatabase(s.helpers, res.session.handle, null, {}); - let res6 = await SessionFunctions.getSessionInformation(s.helpers, res.session.handle); + let res6 = await SessionFunctions.getSessionInformation(s.helpers, res.session.handle, {}); assert.deepStrictEqual(res6.sessionDataInDatabase, {}); }); @@ -865,23 +949,23 @@ describe(`session: ${printPath("[test/session.test.js]")}`, function () { let s = SessionRecipe.getInstanceOrThrowError().recipeInterfaceImpl; //adding jwt payload - let res = await SessionFunctions.createNewSession(s.helpers, "public", new RecipeUserId(""), false, {}, {}); + let res = await SessionFunctions.createNewSession(s.helpers, "public", new RecipeUserId(""), false, {}, {}, {}); - await SessionFunctions.updateAccessTokenPayload(s.helpers, res.session.handle, { key: "value" }); + await SessionFunctions.updateAccessTokenPayload(s.helpers, res.session.handle, { key: "value" }, {}); - let res2 = (await SessionFunctions.getSessionInformation(s.helpers, res.session.handle)) + let res2 = (await SessionFunctions.getSessionInformation(s.helpers, res.session.handle, {})) .customClaimsInAccessTokenPayload; assert.deepStrictEqual(res2, { key: "value" }); //changing the value of jwt payload with the same key - await SessionFunctions.updateAccessTokenPayload(s.helpers, res.session.handle, { key: "value 2" }); + await SessionFunctions.updateAccessTokenPayload(s.helpers, res.session.handle, { key: "value 2" }, {}); - let res3 = (await SessionFunctions.getSessionInformation(s.helpers, res.session.handle)) + let res3 = (await SessionFunctions.getSessionInformation(s.helpers, res.session.handle, {})) .customClaimsInAccessTokenPayload; assert.deepStrictEqual(res3, { key: "value 2" }); //passing invalid session handle when updating jwt payload - assert(!(await SessionFunctions.updateAccessTokenPayload(s.helpers, "random", { key2: "value2" }))); + assert(!(await SessionFunctions.updateAccessTokenPayload(s.helpers, "random", { key2: "value2" }, {}))); }); it("test manipulating jwt payload with new get session method", async function () { @@ -908,21 +992,21 @@ describe(`session: ${printPath("[test/session.test.js]")}`, function () { let s = SessionRecipe.getInstanceOrThrowError().recipeInterfaceImpl; //adding jwt payload - let res = await SessionFunctions.createNewSession(s.helpers, "public", new RecipeUserId(""), false, {}, {}); + let res = await SessionFunctions.createNewSession(s.helpers, "public", new RecipeUserId(""), false, {}, {}, {}); - await SessionFunctions.updateAccessTokenPayload(s.helpers, res.session.handle, { key: "value" }); + await SessionFunctions.updateAccessTokenPayload(s.helpers, res.session.handle, { key: "value" }, {}); - let res2 = await SessionFunctions.getSessionInformation(s.helpers, res.session.handle); + let res2 = await SessionFunctions.getSessionInformation(s.helpers, res.session.handle, {}); assert.deepStrictEqual(res2.customClaimsInAccessTokenPayload, { key: "value" }); //changing the value of jwt payload with the same key - await SessionFunctions.updateAccessTokenPayload(s.helpers, res.session.handle, { key: "value 2" }); + await SessionFunctions.updateAccessTokenPayload(s.helpers, res.session.handle, { key: "value 2" }, {}); - let res3 = await SessionFunctions.getSessionInformation(s.helpers, res.session.handle); + let res3 = await SessionFunctions.getSessionInformation(s.helpers, res.session.handle, {}); assert.deepStrictEqual(res3.customClaimsInAccessTokenPayload, { key: "value 2" }); //passing invalid session handle when updating jwt payload - assert(!(await SessionFunctions.updateAccessTokenPayload(s.helpers, "random", { key2: "value2" }))); + assert(!(await SessionFunctions.updateAccessTokenPayload(s.helpers, "random", { key2: "value2" }, {}))); }); it("test null and undefined values passed for jwt payload", async function () { @@ -941,33 +1025,41 @@ describe(`session: ${printPath("[test/session.test.js]")}`, function () { let s = SessionRecipe.getInstanceOrThrowError().recipeInterfaceImpl; //adding jwt payload - let res = await SessionFunctions.createNewSession(s.helpers, "public", new RecipeUserId(""), false, null, {}); + let res = await SessionFunctions.createNewSession( + s.helpers, + "public", + new RecipeUserId(""), + false, + null, + {}, + {} + ); - let res2 = (await SessionFunctions.getSessionInformation(s.helpers, res.session.handle)) + let res2 = (await SessionFunctions.getSessionInformation(s.helpers, res.session.handle, {})) .customClaimsInAccessTokenPayload; assert.deepStrictEqual(res2, {}); - await SessionFunctions.updateAccessTokenPayload(s.helpers, res.session.handle, { key: "value" }); + await SessionFunctions.updateAccessTokenPayload(s.helpers, res.session.handle, { key: "value" }, {}); - let res3 = (await SessionFunctions.getSessionInformation(s.helpers, res.session.handle)) + let res3 = (await SessionFunctions.getSessionInformation(s.helpers, res.session.handle, {})) .customClaimsInAccessTokenPayload; assert.deepStrictEqual(res3, { key: "value" }); - await SessionFunctions.updateAccessTokenPayload(s.helpers, res.session.handle); + await SessionFunctions.updateAccessTokenPayload(s.helpers, res.session.handle, undefined, {}); - let res4 = (await SessionFunctions.getSessionInformation(s.helpers, res.session.handle, undefined)) + let res4 = (await SessionFunctions.getSessionInformation(s.helpers, res.session.handle, {})) .customClaimsInAccessTokenPayload; assert.deepStrictEqual(res4, {}); - await SessionFunctions.updateAccessTokenPayload(s.helpers, res.session.handle, { key: "value 2" }); + await SessionFunctions.updateAccessTokenPayload(s.helpers, res.session.handle, { key: "value 2" }, {}); - let res5 = (await SessionFunctions.getSessionInformation(s.helpers, res.session.handle)) + let res5 = (await SessionFunctions.getSessionInformation(s.helpers, res.session.handle, {})) .customClaimsInAccessTokenPayload; assert.deepStrictEqual(res5, { key: "value 2" }); - await SessionFunctions.updateAccessTokenPayload(s.helpers, res.session.handle, null); + await SessionFunctions.updateAccessTokenPayload(s.helpers, res.session.handle, null, {}); - let res6 = (await SessionFunctions.getSessionInformation(s.helpers, res.session.handle)) + let res6 = (await SessionFunctions.getSessionInformation(s.helpers, res.session.handle, {})) .customClaimsInAccessTokenPayload; assert.deepStrictEqual(res6, {}); }); @@ -996,29 +1088,37 @@ describe(`session: ${printPath("[test/session.test.js]")}`, function () { let s = SessionRecipe.getInstanceOrThrowError().recipeInterfaceImpl; //adding jwt payload - let res = await SessionFunctions.createNewSession(s.helpers, "public", new RecipeUserId(""), false, null, {}); + let res = await SessionFunctions.createNewSession( + s.helpers, + "public", + new RecipeUserId(""), + false, + null, + {}, + {} + ); - let res2 = await SessionFunctions.getSessionInformation(s.helpers, res.session.handle); + let res2 = await SessionFunctions.getSessionInformation(s.helpers, res.session.handle, {}); assert.deepStrictEqual(res2.customClaimsInAccessTokenPayload, {}); - await SessionFunctions.updateAccessTokenPayload(s.helpers, res.session.handle, { key: "value" }); + await SessionFunctions.updateAccessTokenPayload(s.helpers, res.session.handle, { key: "value" }, {}); - let res3 = await SessionFunctions.getSessionInformation(s.helpers, res.session.handle); + let res3 = await SessionFunctions.getSessionInformation(s.helpers, res.session.handle, {}); assert.deepStrictEqual(res3.customClaimsInAccessTokenPayload, { key: "value" }); - await SessionFunctions.updateAccessTokenPayload(s.helpers, res.session.handle); + await SessionFunctions.updateAccessTokenPayload(s.helpers, res.session.handle, undefined, {}); - let res4 = await SessionFunctions.getSessionInformation(s.helpers, res.session.handle, undefined); + let res4 = await SessionFunctions.getSessionInformation(s.helpers, res.session.handle, {}); assert.deepStrictEqual(res4.customClaimsInAccessTokenPayload, {}); - await SessionFunctions.updateAccessTokenPayload(s.helpers, res.session.handle, { key: "value 2" }); + await SessionFunctions.updateAccessTokenPayload(s.helpers, res.session.handle, { key: "value 2" }, {}); - let res5 = await SessionFunctions.getSessionInformation(s.helpers, res.session.handle); + let res5 = await SessionFunctions.getSessionInformation(s.helpers, res.session.handle, {}); assert.deepStrictEqual(res5.customClaimsInAccessTokenPayload, { key: "value 2" }); - await SessionFunctions.updateAccessTokenPayload(s.helpers, res.session.handle, null); + await SessionFunctions.updateAccessTokenPayload(s.helpers, res.session.handle, null, {}); - let res6 = await SessionFunctions.getSessionInformation(s.helpers, res.session.handle); + let res6 = await SessionFunctions.getSessionInformation(s.helpers, res.session.handle, {}); assert.deepStrictEqual(res6.customClaimsInAccessTokenPayload, {}); }); @@ -1045,6 +1145,7 @@ describe(`session: ${printPath("[test/session.test.js]")}`, function () { new RecipeUserId(""), false, {}, + {}, {} ); @@ -1054,7 +1155,8 @@ describe(`session: ${printPath("[test/session.test.js]")}`, function () { parseJWTWithoutSignatureVerification(response.accessToken.token), undefined, false, - true + true, + {} ); assert(response2.session != undefined); assert.strictEqual(Object.keys(response2.session).length, 6); @@ -1065,7 +1167,8 @@ describe(`session: ${printPath("[test/session.test.js]")}`, function () { parseJWTWithoutSignatureVerification(response.accessToken.token), undefined, true, - true + true, + {} ); assert(response3.session != undefined); assert.strictEqual(Object.keys(response3.session).length, 6); @@ -1106,7 +1209,7 @@ describe(`session: ${printPath("[test/session.test.js]")}`, function () { }); let s = SessionRecipe.getInstanceOrThrowError().recipeInterfaceImpl; - await SessionFunctions.createNewSession(s.helpers, "public", new RecipeUserId(""), false, {}, {}); + await SessionFunctions.createNewSession(s.helpers, "public", new RecipeUserId(""), false, {}, {}, {}); }); it("test that anti-csrf disabled and sameSite strict does now throw an error", async function () { @@ -1126,7 +1229,7 @@ describe(`session: ${printPath("[test/session.test.js]")}`, function () { }); let s = SessionRecipe.getInstanceOrThrowError().recipeInterfaceImpl; - await SessionFunctions.createNewSession(s.helpers, "public", new RecipeUserId(""), false, {}, {}); + await SessionFunctions.createNewSession(s.helpers, "public", new RecipeUserId(""), false, {}, {}, {}); }); it("test that custom user id is returned correctly", async function () { @@ -1159,10 +1262,11 @@ describe(`session: ${printPath("[test/session.test.js]")}`, function () { new RecipeUserId("customuserid"), false, {}, - null + null, + {} ); - let res2 = await SessionFunctions.getSessionInformation(s.helpers, res.session.handle); + let res2 = await SessionFunctions.getSessionInformation(s.helpers, res.session.handle, {}); assert.strictEqual(res2.userId, "customuserid"); }); @@ -1191,8 +1295,16 @@ describe(`session: ${printPath("[test/session.test.js]")}`, function () { let s = SessionRecipe.getInstanceOrThrowError().recipeInterfaceImpl; //adding session data - let res = await SessionFunctions.createNewSession(s.helpers, "public", new RecipeUserId(""), false, {}, null); - let res2 = await SessionFunctions.getSessionInformation(s.helpers, res.session.handle); + let res = await SessionFunctions.createNewSession( + s.helpers, + "public", + new RecipeUserId(""), + false, + {}, + null, + {} + ); + let res2 = await SessionFunctions.getSessionInformation(s.helpers, res.session.handle, {}); assert(typeof res2.userId === "string"); assert(typeof res2.sessionDataInDatabase === "object"); @@ -1232,13 +1344,21 @@ describe(`session: ${printPath("[test/session.test.js]")}`, function () { new RecipeUserId("someid"), false, {}, - null + null, + {} ); - let response = await SessionFunctions.revokeAllSessionsForUser(s.helpers, "someid"); + let response = await SessionFunctions.revokeAllSessionsForUser( + s.helpers, + "someid", + undefined, + undefined, + undefined, + {} + ); assert(response.length === 1); - assert(!(await SessionFunctions.getSessionInformation(s.helpers, res.session.handle))); + assert(!(await SessionFunctions.getSessionInformation(s.helpers, res.session.handle, {}))); }); it("should use override functions in sessioncontainer methods", async function () { diff --git a/test/session/accessTokenVersions.test.js b/test/session/accessTokenVersions.test.js index db46a7945..b4e5d03c7 100644 --- a/test/session/accessTokenVersions.test.js +++ b/test/session/accessTokenVersions.test.js @@ -271,6 +271,7 @@ describe(`AccessToken versions: ${printPath("[test/session/accessTokenVersions.t Session.init({ override: { functions: (oI) => ({ + ...oI, createNewSession: (input) => { input.accessTokenPayload = { ...input.accessTokenPayload, @@ -345,7 +346,8 @@ describe(`AccessToken versions: ${printPath("[test/session/accessTokenVersions.t sub: "asdf", }, userDataInDatabase: {}, - } + }, + {} ); Querier.apiVersion = undefined; @@ -445,7 +447,8 @@ describe(`AccessToken versions: ${printPath("[test/session/accessTokenVersions.t sub: "asdf", }, userDataInDatabase: {}, - } + }, + {} ); Querier.apiVersion = undefined; @@ -511,7 +514,8 @@ describe(`AccessToken versions: ${printPath("[test/session/accessTokenVersions.t enableAntiCsrf: false, userDataInJWT: {}, userDataInDatabase: {}, - } + }, + {} ); Querier.apiVersion = undefined; @@ -569,7 +573,8 @@ describe(`AccessToken versions: ${printPath("[test/session/accessTokenVersions.t enableAntiCsrf: false, userDataInJWT: {}, userDataInDatabase: {}, - } + }, + {} ); Querier.apiVersion = undefined; @@ -657,7 +662,8 @@ describe(`AccessToken versions: ${printPath("[test/session/accessTokenVersions.t enableAntiCsrf: false, userDataInJWT: {}, userDataInDatabase: {}, - } + }, + {} ); Querier.apiVersion = undefined; @@ -724,7 +730,8 @@ describe(`AccessToken versions: ${printPath("[test/session/accessTokenVersions.t enableAntiCsrf: false, userDataInJWT: {}, userDataInDatabase: {}, - } + }, + {} ); Querier.apiVersion = undefined; @@ -978,7 +985,8 @@ describe(`AccessToken versions: ${printPath("[test/session/accessTokenVersions.t enableAntiCsrf: false, userDataInJWT: {}, userDataInDatabase: {}, - } + }, + {} ); Querier.apiVersion = undefined; @@ -1034,7 +1042,8 @@ describe(`AccessToken versions: ${printPath("[test/session/accessTokenVersions.t sub: "asdf", }, userDataInDatabase: {}, - } + }, + {} ); Querier.apiVersion = undefined; diff --git a/test/session/overwriteSessionDuringSignIn.test.js b/test/session/overwriteSessionDuringSignIn.test.js new file mode 100644 index 000000000..6c2c9e2a8 --- /dev/null +++ b/test/session/overwriteSessionDuringSignIn.test.js @@ -0,0 +1,313 @@ +/* Copyright (c) 2021, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +const { printPath, setupST, startST, killAllST, cleanST, extractInfoFromResponse, resetAll } = require("../utils"); +const assert = require("assert"); +const { Querier } = require("../../lib/build/querier"); +const express = require("express"); +const request = require("supertest"); +const { ProcessState, PROCESS_STATE } = require("../../lib/build/processState"); +const SuperTokens = require("../../"); +const Session = require("../../recipe/session"); +const EmailPassword = require("../../recipe/emailpassword"); +const { middleware, errorHandler } = require("../../framework/express"); +const { json } = require("body-parser"); + +describe(`overwriteSessionDuringSignIn config: ${printPath( + "[test/session/overwriteSessionDuringSignIn.test.js]" +)}`, function () { + beforeEach(async function () { + await killAllST(); + await setupST(); + ProcessState.getInstance().reset(); + }); + + after(async function () { + await killAllST(); + await cleanST(); + }); + + describe("createNewSession", () => { + it("test default", async function () { + const connectionURI = await startST(); + SuperTokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [EmailPassword.init(), Session.init({})], + }); + + const app = getTestExpressApp(); + + await EmailPassword.signUp("public", "test@example.com", "password"); + + let res = await new Promise((resolve) => + request(app) + .post("/auth/signin") + .send({ + formFields: [ + { + id: "password", + value: "password", + }, + { + id: "email", + value: "test@example.com", + }, + ], + }) + .expect(200) + .end((err, res) => { + if (err) { + resolve(undefined); + } else { + resolve(res); + } + }) + ); + + let cookies = extractInfoFromResponse(res); + + assert(cookies.accessTokenFromAny !== undefined); + assert(cookies.refreshTokenFromAny !== undefined); + assert(cookies.frontToken !== undefined); + + let accessToken = cookies.accessTokenFromAny; + + res = await new Promise((resolve) => + request(app) + .post("/auth/signin") + .set("Authorization", "Bearer " + accessToken) + .send({ + formFields: [ + { + id: "password", + value: "password", + }, + { + id: "email", + value: "test@example.com", + }, + ], + }) + .expect(200) + .end((err, res) => { + if (err) { + resolve(undefined); + } else { + resolve(res); + } + }) + ); + + cookies = extractInfoFromResponse(res); + assert(cookies.accessTokenFromAny === undefined); + }); + + it("test false", async function () { + const connectionURI = await startST(); + SuperTokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [EmailPassword.init(), Session.init({ overwriteSessionDuringSignIn: false })], + }); + + const app = getTestExpressApp(); + + await EmailPassword.signUp("public", "test@example.com", "password"); + + let res = await new Promise((resolve) => + request(app) + .post("/auth/signin") + .send({ + formFields: [ + { + id: "password", + value: "password", + }, + { + id: "email", + value: "test@example.com", + }, + ], + }) + .expect(200) + .end((err, res) => { + if (err) { + resolve(undefined); + } else { + resolve(res); + } + }) + ); + + let cookies = extractInfoFromResponse(res); + + assert(cookies.accessTokenFromAny !== undefined); + assert(cookies.refreshTokenFromAny !== undefined); + assert(cookies.frontToken !== undefined); + + let accessToken = cookies.accessTokenFromAny; + + res = await new Promise((resolve) => + request(app) + .post("/auth/signin") + .set("Authorization", "Bearer " + accessToken) + .send({ + formFields: [ + { + id: "password", + value: "password", + }, + { + id: "email", + value: "test@example.com", + }, + ], + }) + .expect(200) + .end((err, res) => { + if (err) { + resolve(undefined); + } else { + resolve(res); + } + }) + ); + + cookies = extractInfoFromResponse(res); + assert(cookies.accessTokenFromAny === undefined); + }); + + it("test true", async function () { + const connectionURI = await startST(); + SuperTokens.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [EmailPassword.init(), Session.init({ overwriteSessionDuringSignIn: true })], + }); + + const app = getTestExpressApp(); + + await EmailPassword.signUp("public", "test@example.com", "password"); + + let res = await new Promise((resolve) => + request(app) + .post("/auth/signin") + .send({ + formFields: [ + { + id: "password", + value: "password", + }, + { + id: "email", + value: "test@example.com", + }, + ], + }) + .expect(200) + .end((err, res) => { + if (err) { + resolve(undefined); + } else { + resolve(res); + } + }) + ); + + let cookies = extractInfoFromResponse(res); + + assert(cookies.accessTokenFromAny !== undefined); + assert(cookies.refreshTokenFromAny !== undefined); + assert(cookies.frontToken !== undefined); + + let accessToken = cookies.accessTokenFromAny; + + res = await new Promise((resolve) => + request(app) + .post("/auth/signin") + .set("Authorization", "Bearer " + accessToken) + .send({ + formFields: [ + { + id: "password", + value: "password", + }, + { + id: "email", + value: "test@example.com", + }, + ], + }) + .expect(200) + .end((err, res) => { + if (err) { + resolve(undefined); + } else { + resolve(res); + } + }) + ); + + cookies = extractInfoFromResponse(res); + assert(cookies.accessTokenFromAny !== undefined); + }); + }); +}); + +function getTestExpressApp() { + const app = express(); + + app.use(middleware()); + app.use(json()); + + app.post("/create", async (req, res) => { + const userId = req.body.userId || ""; + try { + await Session.createNewSession( + req, + res, + "public", + SuperTokens.convertToRecipeUserId(userId), + req.body.payload, + {}, + false // alwaysOverwriteSessionInRequest + ); + res.status(200).send(""); + } catch (ex) { + res.status(400).json({ message: ex.message }); + } + }); + + app.use(errorHandler()); + return app; +} diff --git a/test/sessionAccessTokenSigningKeyUpdate.test.js b/test/sessionAccessTokenSigningKeyUpdate.test.js index ef38b07b0..5ee2d449b 100644 --- a/test/sessionAccessTokenSigningKeyUpdate.test.js +++ b/test/sessionAccessTokenSigningKeyUpdate.test.js @@ -89,6 +89,7 @@ describe(`sessionAccessTokenSigningKeyUpdate: ${printPath( new RecipeUserId(""), false, {}, + {}, {} ); @@ -100,7 +101,8 @@ describe(`sessionAccessTokenSigningKeyUpdate: ${printPath( parseJWTWithoutSignatureVerification(response.accessToken.token), response.antiCsrfToken, true, - false + false, + {} ); let verifyState = await ProcessState.getInstance().waitForEvent( @@ -122,7 +124,8 @@ describe(`sessionAccessTokenSigningKeyUpdate: ${printPath( parseJWTWithoutSignatureVerification(response.accessToken.token), response.antiCsrfToken, true, - false + false, + {} ); } catch (err) { if (err.type !== Session.Error.TRY_REFRESH_TOKEN) { @@ -147,7 +150,8 @@ describe(`sessionAccessTokenSigningKeyUpdate: ${printPath( SessionRecipe.getInstanceOrThrowError().recipeInterfaceImpl.helpers, response.refreshToken.token, response.antiCsrfToken, - false + false, + {} ); // Calling refresh doesn't refresh the key cache assert.strictEqual(requestMock.callCount, 1); @@ -163,7 +167,8 @@ describe(`sessionAccessTokenSigningKeyUpdate: ${printPath( parseJWTWithoutSignatureVerification(response2.accessToken.token), response2.antiCsrfToken, true, - false + false, + {} ); // This should have refreshed the keys because of the cache miss @@ -224,6 +229,7 @@ describe(`sessionAccessTokenSigningKeyUpdate: ${printPath( new RecipeUserId(""), false, {}, + {}, {} ); @@ -233,6 +239,7 @@ describe(`sessionAccessTokenSigningKeyUpdate: ${printPath( new RecipeUserId(""), false, {}, + {}, {} ); @@ -244,6 +251,7 @@ describe(`sessionAccessTokenSigningKeyUpdate: ${printPath( new RecipeUserId(""), false, {}, + {}, {} ); @@ -253,7 +261,8 @@ describe(`sessionAccessTokenSigningKeyUpdate: ${printPath( parseJWTWithoutSignatureVerification(newSession.accessToken.token), newSession.antiCsrfToken, true, - false + false, + {} ); let verifyState = await ProcessState.getInstance().waitForEvent( @@ -308,6 +317,7 @@ describe(`sessionAccessTokenSigningKeyUpdate: ${printPath( new RecipeUserId(""), false, {}, + {}, {} ); @@ -319,6 +329,7 @@ describe(`sessionAccessTokenSigningKeyUpdate: ${printPath( new RecipeUserId(""), false, {}, + {}, {} ); @@ -328,7 +339,8 @@ describe(`sessionAccessTokenSigningKeyUpdate: ${printPath( parseJWTWithoutSignatureVerification(response.accessToken.token), response.antiCsrfToken, true, - false + false, + {} ); let verifyState = await ProcessState.getInstance().waitForEvent( @@ -347,7 +359,8 @@ describe(`sessionAccessTokenSigningKeyUpdate: ${printPath( parseJWTWithoutSignatureVerification(response2.accessToken.token), response2.antiCsrfToken, true, - false + false, + {} ); // Old core versions should throw here because the signing key was updated } catch (err) { @@ -391,7 +404,7 @@ describe(`sessionAccessTokenSigningKeyUpdate: ${printPath( "public", new RecipeUserId(""), false, - + {}, {}, {} ); @@ -404,6 +417,7 @@ describe(`sessionAccessTokenSigningKeyUpdate: ${printPath( new RecipeUserId(""), false, {}, + {}, {} ); @@ -413,7 +427,8 @@ describe(`sessionAccessTokenSigningKeyUpdate: ${printPath( parseJWTWithoutSignatureVerification(response.accessToken.token), response.antiCsrfToken, true, - false + false, + {} ); let verifyState = await ProcessState.getInstance().waitForEvent( @@ -480,6 +495,7 @@ describe(`sessionAccessTokenSigningKeyUpdate: ${printPath( new RecipeUserId(""), false, {}, + {}, {} ); @@ -489,7 +505,8 @@ describe(`sessionAccessTokenSigningKeyUpdate: ${printPath( parseJWTWithoutSignatureVerification(session.accessToken.token), session.antiCsrfToken, true, - false + false, + {} ); let verifyState3 = await ProcessState.getInstance().waitForEvent( @@ -517,7 +534,8 @@ describe(`sessionAccessTokenSigningKeyUpdate: ${printPath( parseJWTWithoutSignatureVerification(session.accessToken.token), session.antiCsrfToken, true, - false + false, + {} ); let verifyState3 = await ProcessState.getInstance().waitForEvent( @@ -533,6 +551,7 @@ describe(`sessionAccessTokenSigningKeyUpdate: ${printPath( new RecipeUserId(""), false, {}, + {}, {} ); @@ -544,7 +563,8 @@ describe(`sessionAccessTokenSigningKeyUpdate: ${printPath( parseJWTWithoutSignatureVerification(session2.accessToken.token), session2.antiCsrfToken, true, - false + false, + {} ); let verifyState3 = await ProcessState.getInstance().waitForEvent( @@ -567,7 +587,8 @@ describe(`sessionAccessTokenSigningKeyUpdate: ${printPath( parseJWTWithoutSignatureVerification(session2.accessToken.token), session2.antiCsrfToken, true, - false + false, + {} ); let verifyState3 = await ProcessState.getInstance().waitForEvent( @@ -587,7 +608,8 @@ describe(`sessionAccessTokenSigningKeyUpdate: ${printPath( parseJWTWithoutSignatureVerification(session.accessToken.token), session.antiCsrfToken, true, - false + false, + {} ); fail(); } catch (err) { @@ -639,6 +661,7 @@ describe(`sessionAccessTokenSigningKeyUpdate: ${printPath( new RecipeUserId(""), false, {}, + {}, {} ); @@ -648,7 +671,8 @@ describe(`sessionAccessTokenSigningKeyUpdate: ${printPath( parseJWTWithoutSignatureVerification(session.accessToken.token), session.antiCsrfToken, true, - false + false, + {} ); let verifyState3 = await ProcessState.getInstance().waitForEvent( @@ -668,7 +692,8 @@ describe(`sessionAccessTokenSigningKeyUpdate: ${printPath( parseJWTWithoutSignatureVerification(session.accessToken.token), session.antiCsrfToken, true, - false + false, + {} ); let verifyState3 = await ProcessState.getInstance().waitForEvent( diff --git a/test/totp/recipeFunctions.test.js b/test/totp/recipeFunctions.test.js index 2599deb03..16787eb14 100644 --- a/test/totp/recipeFunctions.test.js +++ b/test/totp/recipeFunctions.test.js @@ -114,7 +114,7 @@ describe(`recipeFunctions: ${printPath("[test/totp/recipeFunctions.test.js]")}`, assert(deviceRes.secret !== undefined); assert(deviceRes.qrCodeString !== undefined); - const updateRes = await Totp.removeDevice("testUserId", deviceRes.deviceName, "newDeviceName"); + const updateRes = await Totp.removeDevice("testUserId", deviceRes.deviceName); assert.equal(updateRes.status, "OK"); const listRes = await Totp.listDevices("testUserId"); diff --git a/test/userContext.test.js b/test/userContext.test.js index ee9e838f7..b7cdd143e 100644 --- a/test/userContext.test.js +++ b/test/userContext.test.js @@ -469,6 +469,6 @@ describe(`userContext: ${printPath("[test/userContext.test.js]")}`, function () totalCount += count; } - assert(totalCount === 23); + assert(totalCount === 22); }); }); diff --git a/test/utils.js b/test/utils.js index 5647ca149..228340584 100644 --- a/test/utils.js +++ b/test/utils.js @@ -32,6 +32,7 @@ let JWTRecipe = require("..//lib/build/recipe/jwt/recipe").default; const UserMetadataRecipe = require("../lib/build/recipe/usermetadata/recipe").default; let PasswordlessRecipe = require("..//lib/build/recipe/passwordless/recipe").default; let MultitenancyRecipe = require("../lib/build/recipe/multitenancy/recipe").default; +let MultiFactorAuthRecipe = require("../lib/build/recipe/multifactorauth/recipe").default; const UserRolesRecipe = require("../lib/build/recipe/userroles/recipe").default; let { ProcessState } = require("../lib/build/processState"); let { Querier } = require("../lib/build/querier"); @@ -270,6 +271,7 @@ module.exports.resetAll = function (disableLogging = true) { ProcessState.getInstance().reset(); MultitenancyRecipe.reset(); TotpRecipe.reset(); + MultiFactorAuthRecipe.reset(); if (disableLogging) { debug.disable(); } diff --git a/test/with-typescript/index.ts b/test/with-typescript/index.ts index ea8d65c3b..65f28e9b7 100644 --- a/test/with-typescript/index.ts +++ b/test/with-typescript/index.ts @@ -919,7 +919,7 @@ Multitenancy.init({ }, }); -import { HTTPMethod, TypeInput } from "../../types"; +import { HTTPMethod, TypeInput, UserContext } from "../../types"; import { TypeInput as SessionTypeInput } from "../../recipe/session/types"; import { TypeInput as EPTypeInput } from "../../recipe/emailpassword/types"; import SuperTokensError from "../../lib/build/error"; @@ -1173,6 +1173,7 @@ Supertokens.init({ { email: input.email, }, + undefined, input.userContext ) ).length === 0 @@ -1353,7 +1354,13 @@ Session.init({ input.accessTokenPayload = stringClaim.removeFromPayload(input.accessTokenPayload); input.accessTokenPayload = { ...input.accessTokenPayload, - ...(await boolClaim.build(input.userId, input.recipeUserId, input.tenantId, input.userContext)), + ...(await boolClaim.build( + input.userId, + input.recipeUserId, + input.tenantId, + input.accessTokenPayload, + input.userContext + )), lastTokenRefresh: Date.now(), }; return originalImplementation.createNewSession(input); @@ -1371,7 +1378,7 @@ Session.validateClaimsForSessionHandle("asdf", (globalClaimValidators) => [ Session.validateClaimsForSessionHandle( "asdf", (globalClaimValidators, info) => [...globalClaimValidators, boolClaim.validators.isTrue(info.expiry)], - { test: 1 } + { test: 1, ...({} as UserContext) } ); EmailVerification.sendEmail({ @@ -1404,7 +1411,7 @@ ThirdPartyEmailPassword.sendEmail({ id: "", recipeUserId: Supertokens.convertToRecipeUserId(""), }, - userContext: {}, + userContext: {} as UserContext, }); ThirdPartyPasswordless.sendEmail({ @@ -1422,7 +1429,7 @@ ThirdPartyPasswordless.sendEmail({ email: "", type: "PASSWORDLESS_LOGIN", preAuthSessionId: "", - userContext: {}, + userContext: {} as UserContext, }); ThirdPartyPasswordless.sendSms({ @@ -1440,7 +1447,7 @@ ThirdPartyPasswordless.sendSms({ phoneNumber: "", type: "PASSWORDLESS_LOGIN", preAuthSessionId: "", - userContext: {}, + userContext: {} as UserContext, }); Supertokens.init({ @@ -1563,14 +1570,13 @@ Passwordless.init({ let user = await Passwordless.signInUp({ tenantId: "test", phoneNumber: "TEST_PHONE_NUMBER", - userContext: { calledManually: true }, + userContext: { calledManually: true, ...({} as UserContext) }, }); return { status: "OK", createdNewRecipeUser: user.createdNewRecipeUser, recipeUserId: user.recipeUserId, user: user.user, - isValidFirstFactorForTenant: user.isValidFirstFactorForTenant, }; } } @@ -1871,8 +1877,9 @@ Supertokens.init({ }); // const noMFARequired -MultiFactorAuth.MultiFactorAuthClaim.validators.passesMFARequirements([]); -MultiFactorAuth.MultiFactorAuthClaim.validators.passesMFARequirements([ +MultiFactorAuth.MultiFactorAuthClaim.validators.hasCompletedDefaultFactors(); +MultiFactorAuth.MultiFactorAuthClaim.validators.hasCompletedFactors([]); +MultiFactorAuth.MultiFactorAuthClaim.validators.hasCompletedFactors([ { oneOf: ["emailpassword", "thirdparty"] }, // We can include the first factors here... that feels a bit weird but it works. { oneOf: ["totp", "otp-phone"] }, // We require either totp or otp-phone ]); @@ -1933,7 +1940,21 @@ Supertokens.init({ }), }, }), - Session.init(), + Session.init({ + override: { + functions: (oI) => ({ + ...oI, + createNewSession: async (input) => { + const resp = await oI.createNewSession(input); + if (input.userContext.shouldCompleteTOTP) { + // this is a stand-in for the "remember me" check + await MultiFactorAuth.markFactorAsCompleteInSession(resp, "totp", input.userContext); + } + return resp; + }, + }), + }, + }), ], });