diff --git a/.circleci/config_continue.yml b/.circleci/config_continue.yml index 49dcb27fd..7a2e18b78 100644 --- a/.circleci/config_continue.yml +++ b/.circleci/config_continue.yml @@ -36,6 +36,24 @@ jobs: - run: update-alternatives --install "/usr/bin/javac" "javac" "/usr/java/jdk-15.0.1/bin/javac" 2 - run: (cd .circleci/ && ./doUnitTests.sh << parameters.cdi-version >>) - slack/status + test-backend-sdk-testing: + docker: + - image: rishabhpoddar/supertokens_node_driver_testing_node_20 + resource_class: large + parameters: + cdi-version: + type: string + fdi-version: + type: string + steps: + - checkout + - run: echo "127.0.0.1 localhost.org" >> /etc/hosts + - run: apt-get install lsof + - run: npm i -d --force + - run: update-alternatives --install "/usr/bin/java" "java" "/usr/java/jdk-15.0.1/bin/java" 2 + - run: update-alternatives --install "/usr/bin/javac" "javac" "/usr/java/jdk-15.0.1/bin/javac" 2 + - run: (cd .circleci/ && ./doBackendSDKTests.sh << parameters.cdi-version >> << parameters.fdi-version >>) + - slack/status test-website: docker: - image: rishabhpoddar/supertokens_website_sdk_testing @@ -114,6 +132,20 @@ workflows: matrix: parameters: cdi-version: placeholder + - test-backend-sdk-testing: + requires: + - test-dev-tag-as-not-passed + context: + - slack-notification + filters: + tags: + only: /dev-v[0-9]+(\.[0-9]+)*/ + branches: + only: /test-cicd\/.*/ + matrix: + parameters: + cdi-version: placeholder + fdi-version: placeholder - test-website: requires: - test-dev-tag-as-not-passed @@ -143,6 +175,7 @@ workflows: - test-success: requires: - test-unit + - test-backend-sdk-testing - test-website - test-authreact context: diff --git a/.circleci/doBackendSDKTests.sh b/.circleci/doBackendSDKTests.sh new file mode 100755 index 000000000..984a0b79b --- /dev/null +++ b/.circleci/doBackendSDKTests.sh @@ -0,0 +1,38 @@ +echo "Starting tests for CDI $1"; + +if [ -z "$SUPERTOKENS_API_KEY" ]; then + echo "SUPERTOKENS_API_KEY not set" + exit 1 +fi + +coreDriverVersion=$1 +coreDriverVersion=`echo $coreDriverVersion | tr -d '"'` + +frontendDriverVersion=$2 + +coreFree=`curl -s -X GET \ +"https://api.supertokens.io/0/core-driver-interface/dependency/core/latest?password=$SUPERTOKENS_API_KEY&planType=FREE&mode=DEV&version=$coreDriverVersion&driverName=node" \ +-H 'api-version: 1'` +if [[ `echo $coreFree | jq .core` == "null" ]] +then + echo "fetching latest X.Y version for core given core-driver-interface X.Y version: $coreDriverVersion, planType: FREE gave response: $coreFree. Please make sure all relevant cores have been pushed." + exit 1 +fi +coreFree=$(echo $coreFree | jq .core | tr -d '"') + +cd .. +./test/testExports.sh +if [[ $? -ne 0 ]] +then + echo "export test failed... exiting!" + exit 1 +fi +cd .circleci + +./setupAndTestBackendSDKWithFreeCore.sh $coreFree $coreDriverVersion $frontendDriverVersion +if [[ $? -ne 0 ]] +then + echo "test failed... exiting!" + exit 1 +fi +rm -rf ../../supertokens-root \ No newline at end of file diff --git a/.circleci/setupAndTestBackendSDKWithFreeCore.sh b/.circleci/setupAndTestBackendSDKWithFreeCore.sh new file mode 100755 index 000000000..a4a563b0b --- /dev/null +++ b/.circleci/setupAndTestBackendSDKWithFreeCore.sh @@ -0,0 +1,81 @@ +coreInfo=`curl -s -X GET \ +"https://api.supertokens.io/0/core/latest?password=$SUPERTOKENS_API_KEY&planType=FREE&mode=DEV&version=$1" \ +-H 'api-version: 0'` +if [[ `echo $coreInfo | jq .tag` == "null" ]] +then + echo "fetching latest X.Y.Z version for core, X.Y version: $1, planType: FREE gave response: $coreInfo" + exit 1 +fi +coreTag=$(echo $coreInfo | jq .tag | tr -d '"') +coreVersion=$(echo $coreInfo | jq .version | tr -d '"') + +pluginInterfaceVersionXY=`curl -s -X GET \ +"https://api.supertokens.io/0/core/dependency/plugin-interface/latest?password=$SUPERTOKENS_API_KEY&planType=FREE&mode=DEV&version=$1" \ +-H 'api-version: 0'` +if [[ `echo $pluginInterfaceVersionXY | jq .pluginInterface` == "null" ]] +then + echo "fetching latest X.Y version for plugin-interface, given core X.Y version: $1, planType: FREE gave response: $pluginInterfaceVersionXY" + exit 1 +fi +pluginInterfaceVersionXY=$(echo $pluginInterfaceVersionXY | jq .pluginInterface | tr -d '"') + +pluginInterfaceInfo=`curl -s -X GET \ +"https://api.supertokens.io/0/plugin-interface/latest?password=$SUPERTOKENS_API_KEY&planType=FREE&mode=DEV&version=$pluginInterfaceVersionXY" \ +-H 'api-version: 0'` +if [[ `echo $pluginInterfaceInfo | jq .tag` == "null" ]] +then + echo "fetching latest X.Y.Z version for plugin-interface, X.Y version: $pluginInterfaceVersionXY, planType: FREE gave response: $pluginInterfaceInfo" + exit 1 +fi +pluginInterfaceTag=$(echo $pluginInterfaceInfo | jq .tag | tr -d '"') +pluginInterfaceVersion=$(echo $pluginInterfaceInfo | jq .version | tr -d '"') + +echo "Testing with FREE core: $coreVersion, plugin-interface: $pluginInterfaceVersion" + +cd ../../ +git clone git@github.com:supertokens/supertokens-root.git +cd supertokens-root +if [[ $2 == "2.0" ]] || [[ $2 == "2.1" ]] || [[ $2 == "2.2" ]] +then + git checkout 36e5af1b9a4e3b07247d0cf333cf82a071a78681 +fi +echo -e "core,$1\nplugin-interface,$pluginInterfaceVersionXY" > modules.txt +./loadModules --ssh +cd supertokens-core +git checkout $coreTag +cd ../supertokens-plugin-interface +git checkout $pluginInterfaceTag +cd ../ +echo $SUPERTOKENS_API_KEY > apiPassword +./utils/setupTestEnvLocal +cd ../project/ + +# Set the script to exit on error +set -e + +API_PORT=3030 +ST_CONNECTION_URI=http://localhost:8081 + +# start test-server +pushd test/test-server +npm install +API_PORT=$API_PORT ST_CONNECTION_URI=$ST_CONNECTION_URI npm start & +popd + +frontendDriverVersion=$3 +# run tests +cd ../ +git clone git@github.com:supertokens/backend-sdk-testing.git +cd backend-sdk-testing +git checkout $frontendDriverVersion +npm install +npm run build + +if ! [[ -z "${CIRCLE_NODE_TOTAL}" ]]; then + API_PORT=$API_PORT TEST_MODE=testing SUPERTOKENS_CORE_TAG=$coreTag NODE_PORT=8081 INSTALL_PATH=../supertokens-root npx mocha --node-option no-experimental-fetch -r test/fetch-polyfill.mjs --no-config --timeout 500000 $(npx mocha-split-tests -r ./runtime.log -t $CIRCLE_NODE_TOTAL -g $CIRCLE_NODE_INDEX -f 'test/**/*.test.js') +else + API_PORT=$API_PORT TEST_MODE=testing SUPERTOKENS_CORE_TAG=$coreTag NODE_PORT=8081 INSTALL_PATH=../supertokens-root npm test +fi + +# kill test-server +kill $(lsof -t -i:$API_PORT) diff --git a/.circleci/setupAndTestWithFreeCore.sh b/.circleci/setupAndTestWithFreeCore.sh index 258b0a6d5..83b0f8201 100755 --- a/.circleci/setupAndTestWithFreeCore.sh +++ b/.circleci/setupAndTestWithFreeCore.sh @@ -58,36 +58,3 @@ if ! [[ -z "${CIRCLE_NODE_TOTAL}" ]]; then else TEST_MODE=testing SUPERTOKENS_CORE_TAG=$coreTag NODE_PORT=8081 INSTALL_PATH=../supertokens-root npm test fi - -API_PORT=3030 -ST_CONNECTION_URI=http://localhost:8081 - -# start test-server -pushd test/test-server -npm install -API_PORT=$API_PORT ST_CONNECTION_URI=$ST_CONNECTION_URI npm start & -popd - -# lets read frontendDriverInterfaceSupported -frontendDriverJson=`cat ./frontendDriverInterfaceSupported.json` -# get versions -frontendDriverArray=`echo $frontendDriverJson | jq ".versions"` -# use latest version -frontendDriverVersion=`echo $frontendDriverArray | jq ".[-1]" | tr -d '"'` - -# run tests -cd ../ -git clone git@github.com:supertokens/backend-sdk-testing.git -cd backend-sdk-testing -git checkout $frontendDriverVersion -npm install -npm run build - -if ! [[ -z "${CIRCLE_NODE_TOTAL}" ]]; then - API_PORT=$API_PORT TEST_MODE=testing SUPERTOKENS_CORE_TAG=$coreTag NODE_PORT=8081 INSTALL_PATH=../supertokens-root npx mocha --node-option no-experimental-fetch -r test/fetch-polyfill.mjs --no-config --timeout 500000 $(npx mocha-split-tests -r ./runtime.log -t $CIRCLE_NODE_TOTAL -g $CIRCLE_NODE_INDEX -f 'test/**/*.test.js') -else - API_PORT=$API_PORT TEST_MODE=testing SUPERTOKENS_CORE_TAG=$coreTag NODE_PORT=8081 INSTALL_PATH=../supertokens-root npm test -fi - -# kill test-server -kill $(lsof -t -i:$API_PORT) \ No newline at end of file diff --git a/.gitignore b/.gitignore index a5e771d2c..b8649d610 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ /node_modules +test/test-server/node_modules /examples/**/node_modules .DS_Store /.history @@ -12,4 +13,4 @@ releasePassword /test_report /temp_test_exports /temp_* -/.nyc_output \ No newline at end of file +/.nyc_output diff --git a/CHANGELOG.md b/CHANGELOG.md index 51d786670..f7dfac511 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [unreleased] +## UNRELEASED + +- Added OAuth2Provider recipe + +## [20.0.2] - 2024-08-08 + +- Fixes an issue where `shouldDoAutomaticAccountLinking` was called without a primary user when linking in some cases. + +## [20.0.1] - 2024-08-05 + +- Fixes an issue with `removeFromPayloadByMerge_internal` for `MultiFactorAuthClaim` where it was not retaining other claims while removing the claim from the payload. +- Updates testing with backend-sdk-testing repo to run against all supported FDI versions. + ## [20.0.0] - 2024-07-24 ### Changes @@ -306,6 +319,10 @@ for (const tenant of tenantsRes.tenants) { - `refreshPOST` and `refreshSession` now clears all user tokens upon CSRF failures and if no tokens are found. See the latest comment on https://github.com/supertokens/supertokens-node/issues/141 for more details. +## [18.0.2] - 2024-07-09 + +- `refreshPOST` and `refreshSession` now clears all user tokens upon CSRF failures and if no tokens are found. See the latest comment on https://github.com/supertokens/supertokens-node/issues/141 for more details. + ## [18.0.1] - 2024-06-19 ### Fixes diff --git a/docs/classes/framework.BaseRequest.html b/docs/classes/framework.BaseRequest.html index e3689decc..731db467e 100644 --- a/docs/classes/framework.BaseRequest.html +++ b/docs/classes/framework.BaseRequest.html @@ -1 +1 @@ -BaseRequest | supertokens-node
Options
All
  • Public
  • Public/Protected
  • All
Menu

Class BaseRequest Abstract

Hierarchy

Index

Constructors

Properties

getCookieValue: ((key_: string) => undefined | string)

Type declaration

    • (key_: string): undefined | string
    • Parameters

      • key_: string

      Returns undefined | string

getHeaderValue: ((key: string) => undefined | string)

Type declaration

    • (key: string): undefined | string
    • Parameters

      • key: string

      Returns undefined | string

getKeyValueFromQuery: ((key: string) => undefined | string)

Type declaration

    • (key: string): undefined | string
    • Parameters

      • key: string

      Returns undefined | string

getMethod: (() => HTTPMethod)

Type declaration

    • (): HTTPMethod
    • Returns HTTPMethod

getOriginalURL: (() => string)

Type declaration

    • (): string
    • Returns string

original: any
parsedJSONBody: any
parsedUrlEncodedFormData: any
wrapperUsed: boolean

Methods

  • getFormData(): Promise<any>
  • getFormDataFromRequestBody(): Promise<any>
  • getJSONBody(): Promise<any>
  • getJSONFromRequestBody(): Promise<any>

Generated using TypeDoc

\ No newline at end of file +BaseRequest | supertokens-node
Options
All
  • Public
  • Public/Protected
  • All
Menu

Class BaseRequest Abstract

Hierarchy

Index

Constructors

Properties

getCookieValue: ((key_: string) => undefined | string)

Type declaration

    • (key_: string): undefined | string
    • Parameters

      • key_: string

      Returns undefined | string

getHeaderValue: ((key: string) => undefined | string)

Type declaration

    • (key: string): undefined | string
    • Parameters

      • key: string

      Returns undefined | string

getKeyValueFromQuery: ((key: string) => undefined | string)

Type declaration

    • (key: string): undefined | string
    • Parameters

      • key: string

      Returns undefined | string

getMethod: (() => HTTPMethod)

Type declaration

    • (): HTTPMethod
    • Returns HTTPMethod

getOriginalURL: (() => string)

Type declaration

    • (): string
    • Returns string

original: any
parsedJSONBody: any
parsedUrlEncodedFormData: any
wrapperUsed: boolean

Methods

  • getFormData(): Promise<any>
  • getFormDataFromRequestBody(): Promise<any>
  • getJSONBody(): Promise<any>
  • getJSONFromRequestBody(): Promise<any>

Generated using TypeDoc

\ No newline at end of file diff --git a/docs/classes/framework.BaseResponse.html b/docs/classes/framework.BaseResponse.html index 44e400a63..555ac7ec5 100644 --- a/docs/classes/framework.BaseResponse.html +++ b/docs/classes/framework.BaseResponse.html @@ -1 +1 @@ -BaseResponse | supertokens-node
Options
All
  • Public
  • Public/Protected
  • All
Menu

Class BaseResponse Abstract

Hierarchy

Index

Constructors

Properties

original: any
removeHeader: ((key: string) => void)

Type declaration

    • (key: string): void
    • Parameters

      • key: string

      Returns void

sendHTMLResponse: ((html: string) => void)

Type declaration

    • (html: string): void
    • Parameters

      • html: string

      Returns void

sendJSONResponse: ((content: any) => void)

Type declaration

    • (content: any): void
    • Parameters

      • content: any

      Returns void

setCookie: ((key: string, value: string, domain: undefined | string, secure: boolean, httpOnly: boolean, expires: number, path: string, sameSite: "strict" | "lax" | "none") => void)

Type declaration

    • (key: string, value: string, domain: undefined | string, secure: boolean, httpOnly: boolean, expires: number, path: string, sameSite: "strict" | "lax" | "none"): void
    • Parameters

      • key: string
      • value: string
      • domain: undefined | string
      • secure: boolean
      • httpOnly: boolean
      • expires: number
      • path: string
      • sameSite: "strict" | "lax" | "none"

      Returns void

setHeader: ((key: string, value: string, allowDuplicateKey: boolean) => void)

Type declaration

    • (key: string, value: string, allowDuplicateKey: boolean): void
    • Parameters

      • key: string
      • value: string
      • allowDuplicateKey: boolean

      Returns void

setStatusCode: ((statusCode: number) => void)

Type declaration

    • (statusCode: number): void
    • Parameters

      • statusCode: number

      Returns void

wrapperUsed: boolean

Generated using TypeDoc

\ No newline at end of file +BaseResponse | supertokens-node
Options
All
  • Public
  • Public/Protected
  • All
Menu

Class BaseResponse Abstract

Hierarchy

Index

Constructors

Properties

original: any
removeHeader: ((key: string) => void)

Type declaration

    • (key: string): void
    • Parameters

      • key: string

      Returns void

sendHTMLResponse: ((html: string) => void)

Type declaration

    • (html: string): void
    • Parameters

      • html: string

      Returns void

sendJSONResponse: ((content: any) => void)

Type declaration

    • (content: any): void
    • Parameters

      • content: any

      Returns void

setCookie: ((key: string, value: string, domain: undefined | string, secure: boolean, httpOnly: boolean, expires: number, path: string, sameSite: "strict" | "lax" | "none") => void)

Type declaration

    • (key: string, value: string, domain: undefined | string, secure: boolean, httpOnly: boolean, expires: number, path: string, sameSite: "strict" | "lax" | "none"): void
    • Parameters

      • key: string
      • value: string
      • domain: undefined | string
      • secure: boolean
      • httpOnly: boolean
      • expires: number
      • path: string
      • sameSite: "strict" | "lax" | "none"

      Returns void

setHeader: ((key: string, value: string, allowDuplicateKey: boolean) => void)

Type declaration

    • (key: string, value: string, allowDuplicateKey: boolean): void
    • Parameters

      • key: string
      • value: string
      • allowDuplicateKey: boolean

      Returns void

setStatusCode: ((statusCode: number) => void)

Type declaration

    • (statusCode: number): void
    • Parameters

      • statusCode: number

      Returns void

wrapperUsed: boolean

Generated using TypeDoc

\ No newline at end of file diff --git a/docs/classes/framework_custom.CollectingResponse.html b/docs/classes/framework_custom.CollectingResponse.html index 0390f9a2d..54d8bffe2 100644 --- a/docs/classes/framework_custom.CollectingResponse.html +++ b/docs/classes/framework_custom.CollectingResponse.html @@ -1,2 +1,2 @@ -CollectingResponse | supertokens-node
Options
All
  • Public
  • Public/Protected
  • All
Menu

Hierarchy

Index

Constructors

Properties

body?: string
cookies: CookieInfo[]
headers: Headers
original: any
statusCode: number
wrapperUsed: boolean

Methods

  • removeHeader(key: string): void
  • sendHTMLResponse(html: string): void
  • sendJSONResponse(content: any): void
  • setCookie(key: string, value: string, domain: undefined | string, secure: boolean, httpOnly: boolean, expires: number, path: string, sameSite: "strict" | "lax" | "none"): void
  • Parameters

    • key: string
    • value: string
    • domain: undefined | string
    • secure: boolean
    • httpOnly: boolean
    • expires: number
    • path: string
    • sameSite: "strict" | "lax" | "none"

    Returns void

  • setHeader(key: string, value: string, allowDuplicateKey: boolean): void
  • setStatusCode(statusCode: number): void
  • resetPasswordUsingToken(tenantId: string, token: string, newPassword: string, userContext?: Record<string, any>): Promise<{ status: "OK" | "UNKNOWN_USER_ID_ERROR" | "RESET_PASSWORD_INVALID_TOKEN_ERROR" } | { failureReason: string; status: "PASSWORD_POLICY_VIOLATED_ERROR" }>
  • Parameters

    • tenantId: string
    • token: string
    • newPassword: string
    • Optional userContext: Record<string, any>

    Returns Promise<{ status: "OK" | "UNKNOWN_USER_ID_ERROR" | "RESET_PASSWORD_INVALID_TOKEN_ERROR" } | { failureReason: string; status: "PASSWORD_POLICY_VIOLATED_ERROR" }>

  • sendEmail(input: TypeEmailPasswordPasswordResetEmailDeliveryInput & { userContext?: Record<string, any> }): Promise<void>
  • sendResetPasswordEmail(tenantId: string, userId: string, email: string, userContext?: Record<string, any>): Promise<{ status: "OK" | "UNKNOWN_USER_ID_ERROR" }>
  • Parameters

    • tenantId: string
    • userId: string
    • email: string
    • Optional userContext: Record<string, any>

    Returns Promise<{ status: "OK" | "UNKNOWN_USER_ID_ERROR" }>

  • signIn(tenantId: string, email: string, password: string, session?: undefined, userContext?: Record<string, any>): Promise<{ recipeUserId: RecipeUserId; status: "OK"; user: User } | { status: "WRONG_CREDENTIALS_ERROR" }>
  • signIn(tenantId: string, email: string, password: string, session: SessionContainer, userContext?: Record<string, any>): Promise<{ recipeUserId: RecipeUserId; status: "OK"; user: User } | { status: "WRONG_CREDENTIALS_ERROR" } | { reason: "ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR" | "RECIPE_USER_ID_ALREADY_LINKED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR" | "EMAIL_VERIFICATION_REQUIRED" | "SESSION_USER_ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR"; status: "LINKING_TO_SESSION_USER_FAILED" }>
  • Parameters

    • tenantId: string
    • email: string
    • password: string
    • Optional session: undefined
    • Optional userContext: Record<string, any>

    Returns Promise<{ recipeUserId: RecipeUserId; status: "OK"; user: User } | { status: "WRONG_CREDENTIALS_ERROR" }>

  • Parameters

    • tenantId: string
    • email: string
    • password: string
    • session: SessionContainer
    • Optional userContext: Record<string, any>

    Returns Promise<{ recipeUserId: RecipeUserId; status: "OK"; user: User } | { status: "WRONG_CREDENTIALS_ERROR" } | { reason: "ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR" | "RECIPE_USER_ID_ALREADY_LINKED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR" | "EMAIL_VERIFICATION_REQUIRED" | "SESSION_USER_ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR"; status: "LINKING_TO_SESSION_USER_FAILED" }>

  • signUp(tenantId: string, email: string, password: string, session?: undefined, userContext?: Record<string, any>): Promise<{ recipeUserId: RecipeUserId; status: "OK"; user: User } | { status: "EMAIL_ALREADY_EXISTS_ERROR" }>
  • signUp(tenantId: string, email: string, password: string, session: SessionContainer, userContext?: Record<string, any>): Promise<{ recipeUserId: RecipeUserId; status: "OK"; user: User } | { status: "EMAIL_ALREADY_EXISTS_ERROR" } | { reason: "ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR" | "RECIPE_USER_ID_ALREADY_LINKED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR" | "EMAIL_VERIFICATION_REQUIRED" | "SESSION_USER_ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR"; status: "LINKING_TO_SESSION_USER_FAILED" }>
  • Parameters

    • tenantId: string
    • email: string
    • password: string
    • Optional session: undefined
    • Optional userContext: Record<string, any>

    Returns Promise<{ recipeUserId: RecipeUserId; status: "OK"; user: User } | { status: "EMAIL_ALREADY_EXISTS_ERROR" }>

  • Parameters

    • tenantId: string
    • email: string
    • password: string
    • session: SessionContainer
    • Optional userContext: Record<string, any>

    Returns Promise<{ recipeUserId: RecipeUserId; status: "OK"; user: User } | { status: "EMAIL_ALREADY_EXISTS_ERROR" } | { reason: "ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR" | "RECIPE_USER_ID_ALREADY_LINKED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR" | "EMAIL_VERIFICATION_REQUIRED" | "SESSION_USER_ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR"; status: "LINKING_TO_SESSION_USER_FAILED" }>

  • updateEmailOrPassword(input: { applyPasswordPolicy?: boolean; email?: string; password?: string; recipeUserId: RecipeUserId; tenantIdForPasswordPolicy?: string; userContext?: Record<string, any> }): Promise<{ status: "OK" | "EMAIL_ALREADY_EXISTS_ERROR" | "UNKNOWN_USER_ID_ERROR" } | { reason: string; status: "EMAIL_CHANGE_NOT_ALLOWED_ERROR" } | { failureReason: string; status: "PASSWORD_POLICY_VIOLATED_ERROR" }>
  • Parameters

    • input: { applyPasswordPolicy?: boolean; email?: string; password?: string; recipeUserId: RecipeUserId; tenantIdForPasswordPolicy?: string; userContext?: Record<string, any> }
      • Optional applyPasswordPolicy?: boolean
      • Optional email?: string
      • Optional password?: string
      • recipeUserId: RecipeUserId
      • Optional tenantIdForPasswordPolicy?: string
      • Optional userContext?: Record<string, any>

    Returns Promise<{ status: "OK" | "EMAIL_ALREADY_EXISTS_ERROR" | "UNKNOWN_USER_ID_ERROR" } | { reason: string; status: "EMAIL_CHANGE_NOT_ALLOWED_ERROR" } | { failureReason: string; status: "PASSWORD_POLICY_VIOLATED_ERROR" }>

  • verifyCredentials(tenantId: string, email: string, password: string, userContext?: Record<string, any>): Promise<{ status: "OK" | "WRONG_CREDENTIALS_ERROR" }>
  • Parameters

    • tenantId: string
    • email: string
    • password: string
    • Optional userContext: Record<string, any>

    Returns Promise<{ status: "OK" | "WRONG_CREDENTIALS_ERROR" }>

Generated using TypeDoc

\ No newline at end of file diff --git a/docs/classes/recipe_emailverification.default.html b/docs/classes/recipe_emailverification.default.html index 83c69ac42..26f506971 100644 --- a/docs/classes/recipe_emailverification.default.html +++ b/docs/classes/recipe_emailverification.default.html @@ -1 +1 @@ -default | supertokens-node
Options
All
  • Public
  • Public/Protected
  • All
Menu

Hierarchy

  • default

Index

Constructors

Properties

EmailVerificationClaim: EmailVerificationClaimClass = EmailVerificationClaim
Error: typeof default = SuperTokensError
init: ((config: TypeInput) => RecipeListFunction) = Recipe.init

Type declaration

    • (config: TypeInput): RecipeListFunction
    • Parameters

      • config: TypeInput

      Returns RecipeListFunction

Methods

  • createEmailVerificationLink(tenantId: string, recipeUserId: RecipeUserId, email?: string, userContext?: Record<string, any>): Promise<{ link: string; status: "OK" } | { status: "EMAIL_ALREADY_VERIFIED_ERROR" }>
  • createEmailVerificationToken(tenantId: string, recipeUserId: RecipeUserId, email?: string, userContext?: Record<string, any>): Promise<{ status: "OK"; token: string } | { status: "EMAIL_ALREADY_VERIFIED_ERROR" }>
  • isEmailVerified(recipeUserId: RecipeUserId, email?: string, userContext?: Record<string, any>): Promise<boolean>
  • revokeEmailVerificationTokens(tenantId: string, recipeUserId: RecipeUserId, email?: string, userContext?: Record<string, any>): Promise<{ status: string }>
  • sendEmail(input: TypeEmailVerificationEmailDeliveryInput & { userContext?: Record<string, any> }): Promise<void>
  • sendEmailVerificationEmail(tenantId: string, userId: string, recipeUserId: RecipeUserId, email?: string, userContext?: Record<string, any>): Promise<{ status: "OK" } | { status: "EMAIL_ALREADY_VERIFIED_ERROR" }>
  • unverifyEmail(recipeUserId: RecipeUserId, email?: string, userContext?: Record<string, any>): Promise<{ status: string }>
  • verifyEmailUsingToken(tenantId: string, token: string, attemptAccountLinking?: boolean, userContext?: Record<string, any>): Promise<{ status: "OK"; user: UserEmailInfo } | { status: "EMAIL_VERIFICATION_INVALID_TOKEN_ERROR" }>

Generated using TypeDoc

\ No newline at end of file +default | supertokens-node
Options
All
  • Public
  • Public/Protected
  • All
Menu

Hierarchy

  • default

Index

Constructors

Properties

EmailVerificationClaim: EmailVerificationClaimClass = EmailVerificationClaim
Error: typeof default = SuperTokensError
init: ((config: TypeInput) => RecipeListFunction) = Recipe.init

Type declaration

    • (config: TypeInput): RecipeListFunction
    • Parameters

      • config: TypeInput

      Returns RecipeListFunction

Methods

  • createEmailVerificationLink(tenantId: string, recipeUserId: RecipeUserId, email?: string, userContext?: Record<string, any>): Promise<{ link: string; status: "OK" } | { status: "EMAIL_ALREADY_VERIFIED_ERROR" }>
  • createEmailVerificationToken(tenantId: string, recipeUserId: RecipeUserId, email?: string, userContext?: Record<string, any>): Promise<{ status: "OK"; token: string } | { status: "EMAIL_ALREADY_VERIFIED_ERROR" }>
  • isEmailVerified(recipeUserId: RecipeUserId, email?: string, userContext?: Record<string, any>): Promise<boolean>
  • revokeEmailVerificationTokens(tenantId: string, recipeUserId: RecipeUserId, email?: string, userContext?: Record<string, any>): Promise<{ status: string }>
  • sendEmail(input: TypeEmailVerificationEmailDeliveryInput & { userContext?: Record<string, any> }): Promise<void>
  • sendEmailVerificationEmail(tenantId: string, userId: string, recipeUserId: RecipeUserId, email?: string, userContext?: Record<string, any>): Promise<{ status: "OK" } | { status: "EMAIL_ALREADY_VERIFIED_ERROR" }>
  • unverifyEmail(recipeUserId: RecipeUserId, email?: string, userContext?: Record<string, any>): Promise<{ status: string }>
  • verifyEmailUsingToken(tenantId: string, token: string, attemptAccountLinking?: boolean, userContext?: Record<string, any>): Promise<{ status: "OK"; user: UserEmailInfo } | { status: "EMAIL_VERIFICATION_INVALID_TOKEN_ERROR" }>

Generated using TypeDoc

\ No newline at end of file diff --git a/docs/classes/recipe_jwt.default.html b/docs/classes/recipe_jwt.default.html index 1bf6e5c3f..b36f98416 100644 --- a/docs/classes/recipe_jwt.default.html +++ b/docs/classes/recipe_jwt.default.html @@ -1 +1 @@ -default | supertokens-node
Options
All
  • Public
  • Public/Protected
  • All
Menu

Hierarchy

  • default

Index

Constructors

Properties

Methods

Constructors

Properties

init: ((config?: TypeInput) => RecipeListFunction) = Recipe.init

Type declaration

    • (config?: TypeInput): RecipeListFunction
    • Parameters

      • Optional config: TypeInput

      Returns RecipeListFunction

Methods

  • createJWT(payload: any, validitySeconds?: number, useStaticSigningKey?: boolean, userContext?: Record<string, any>): Promise<{ jwt: string; status: "OK" } | { status: "UNSUPPORTED_ALGORITHM_ERROR" }>
  • Parameters

    • payload: any
    • Optional validitySeconds: number
    • Optional useStaticSigningKey: boolean
    • Optional userContext: Record<string, any>

    Returns Promise<{ jwt: string; status: "OK" } | { status: "UNSUPPORTED_ALGORITHM_ERROR" }>

  • getJWKS(userContext?: Record<string, any>): Promise<{ keys: JsonWebKey[]; validityInSeconds?: number }>

Generated using TypeDoc

\ No newline at end of file +default | supertokens-node
Options
All
  • Public
  • Public/Protected
  • All
Menu

Hierarchy

  • default

Index

Constructors

Properties

Methods

Constructors

Properties

init: ((config?: TypeInput) => RecipeListFunction) = Recipe.init

Type declaration

    • (config?: TypeInput): RecipeListFunction
    • Parameters

      • Optional config: TypeInput

      Returns RecipeListFunction

Methods

  • createJWT(payload: any, validitySeconds?: number, useStaticSigningKey?: boolean, userContext?: Record<string, any>): Promise<{ jwt: string; status: "OK" } | { status: "UNSUPPORTED_ALGORITHM_ERROR" }>
  • Parameters

    • payload: any
    • Optional validitySeconds: number
    • Optional useStaticSigningKey: boolean
    • Optional userContext: Record<string, any>

    Returns Promise<{ jwt: string; status: "OK" } | { status: "UNSUPPORTED_ALGORITHM_ERROR" }>

  • getJWKS(userContext?: Record<string, any>): Promise<{ keys: JsonWebKey[]; validityInSeconds?: number }>

Generated using TypeDoc

\ No newline at end of file diff --git a/docs/classes/recipe_multifactorauth.default.html b/docs/classes/recipe_multifactorauth.default.html index 8eea4a71a..95fc4db80 100644 --- a/docs/classes/recipe_multifactorauth.default.html +++ b/docs/classes/recipe_multifactorauth.default.html @@ -1 +1 @@ -default | supertokens-node
Options
All
  • Public
  • Public/Protected
  • All
Menu

Hierarchy

  • default

Index

Constructors

Properties

FactorIds: { EMAILPASSWORD: string; LINK_EMAIL: string; LINK_PHONE: string; OTP_EMAIL: string; OTP_PHONE: string; THIRDPARTY: string; TOTP: string } = FactorIds

Type declaration

  • EMAILPASSWORD: string
  • LINK_EMAIL: string
  • LINK_PHONE: string
  • OTP_EMAIL: string
  • OTP_PHONE: string
  • THIRDPARTY: string
  • TOTP: string
MultiFactorAuthClaim: MultiFactorAuthClaimClass = MultiFactorAuthClaim
init: ((config?: TypeInput) => RecipeListFunction) = Recipe.init

Type declaration

    • (config?: TypeInput): RecipeListFunction
    • Parameters

      • Optional config: TypeInput

      Returns RecipeListFunction

Methods

  • addToRequiredSecondaryFactorsForUser(userId: string, factorId: string, userContext?: Record<string, any>): Promise<void>
  • assertAllowedToSetupFactorElseThrowInvalidClaimError(session: SessionContainer, factorId: string, userContext?: Record<string, any>): Promise<void>
  • getFactorsSetupForUser(userId: string, userContext?: Record<string, any>): Promise<string[]>
  • getMFARequirementsForAuth(session: SessionContainer, userContext?: Record<string, any>): Promise<MFARequirementList>
  • getRequiredSecondaryFactorsForUser(userId: string, userContext?: Record<string, any>): Promise<string[]>
  • markFactorAsCompleteInSession(session: SessionContainer, factorId: string, userContext?: Record<string, any>): Promise<void>
  • removeFromRequiredSecondaryFactorsForUser(userId: string, factorId: string, userContext?: Record<string, any>): Promise<void>

Generated using TypeDoc

\ No newline at end of file +default | supertokens-node
Options
All
  • Public
  • Public/Protected
  • All
Menu

Hierarchy

  • default

Index

Constructors

Properties

FactorIds: { EMAILPASSWORD: string; LINK_EMAIL: string; LINK_PHONE: string; OTP_EMAIL: string; OTP_PHONE: string; THIRDPARTY: string; TOTP: string } = FactorIds

Type declaration

  • EMAILPASSWORD: string
  • LINK_EMAIL: string
  • LINK_PHONE: string
  • OTP_EMAIL: string
  • OTP_PHONE: string
  • THIRDPARTY: string
  • TOTP: string
MultiFactorAuthClaim: MultiFactorAuthClaimClass = MultiFactorAuthClaim
init: ((config?: TypeInput) => RecipeListFunction) = Recipe.init

Type declaration

    • (config?: TypeInput): RecipeListFunction
    • Parameters

      • Optional config: TypeInput

      Returns RecipeListFunction

Methods

  • addToRequiredSecondaryFactorsForUser(userId: string, factorId: string, userContext?: Record<string, any>): Promise<void>
  • assertAllowedToSetupFactorElseThrowInvalidClaimError(session: SessionContainer, factorId: string, userContext?: Record<string, any>): Promise<void>
  • getFactorsSetupForUser(userId: string, userContext?: Record<string, any>): Promise<string[]>
  • getMFARequirementsForAuth(session: SessionContainer, userContext?: Record<string, any>): Promise<MFARequirementList>
  • getRequiredSecondaryFactorsForUser(userId: string, userContext?: Record<string, any>): Promise<string[]>
  • markFactorAsCompleteInSession(session: SessionContainer, factorId: string, userContext?: Record<string, any>): Promise<void>
  • removeFromRequiredSecondaryFactorsForUser(userId: string, factorId: string, userContext?: Record<string, any>): Promise<void>

Generated using TypeDoc

\ No newline at end of file diff --git a/docs/classes/recipe_multitenancy.default.html b/docs/classes/recipe_multitenancy.default.html index 2ccabb278..f594b381b 100644 --- a/docs/classes/recipe_multitenancy.default.html +++ b/docs/classes/recipe_multitenancy.default.html @@ -1 +1 @@ -default | supertokens-node
Options
All
  • Public
  • Public/Protected
  • All
Menu

Hierarchy

  • default

Index

Constructors

Properties

init: ((config?: TypeInput) => RecipeListFunction) = Recipe.init

Type declaration

    • (config?: TypeInput): RecipeListFunction
    • Parameters

      • Optional config: TypeInput

      Returns RecipeListFunction

Methods

  • associateUserToTenant(tenantId: string, recipeUserId: RecipeUserId, userContext?: Record<string, any>): Promise<{ status: "OK"; wasAlreadyAssociated: boolean } | { status: "EMAIL_ALREADY_EXISTS_ERROR" | "UNKNOWN_USER_ID_ERROR" | "PHONE_NUMBER_ALREADY_EXISTS_ERROR" | "THIRD_PARTY_USER_ALREADY_EXISTS_ERROR" } | { reason: string; status: "ASSOCIATION_NOT_ALLOWED_ERROR" }>
  • Parameters

    • tenantId: string
    • recipeUserId: RecipeUserId
    • Optional userContext: Record<string, any>

    Returns Promise<{ status: "OK"; wasAlreadyAssociated: boolean } | { status: "EMAIL_ALREADY_EXISTS_ERROR" | "UNKNOWN_USER_ID_ERROR" | "PHONE_NUMBER_ALREADY_EXISTS_ERROR" | "THIRD_PARTY_USER_ALREADY_EXISTS_ERROR" } | { reason: string; status: "ASSOCIATION_NOT_ALLOWED_ERROR" }>

  • createOrUpdateTenant(tenantId: string, config?: { coreConfig?: {}; firstFactors?: null | string[]; requiredSecondaryFactors?: null | string[] }, userContext?: Record<string, any>): Promise<{ createdNew: boolean; status: "OK" }>
  • Parameters

    • tenantId: string
    • Optional config: { coreConfig?: {}; firstFactors?: null | string[]; requiredSecondaryFactors?: null | string[] }
      • Optional coreConfig?: {}
        • [key: string]: any
      • Optional firstFactors?: null | string[]
      • Optional requiredSecondaryFactors?: null | string[]
    • Optional userContext: Record<string, any>

    Returns Promise<{ createdNew: boolean; status: "OK" }>

  • createOrUpdateThirdPartyConfig(tenantId: string, config: ProviderConfig, skipValidation?: boolean, userContext?: Record<string, any>): Promise<{ createdNew: boolean; status: "OK" }>
  • Parameters

    • tenantId: string
    • config: ProviderConfig
    • Optional skipValidation: boolean
    • Optional userContext: Record<string, any>

    Returns Promise<{ createdNew: boolean; status: "OK" }>

  • deleteTenant(tenantId: string, userContext?: Record<string, any>): Promise<{ didExist: boolean; status: "OK" }>
  • deleteThirdPartyConfig(tenantId: string, thirdPartyId: string, userContext?: Record<string, any>): Promise<{ didConfigExist: boolean; status: "OK" }>
  • Parameters

    • tenantId: string
    • thirdPartyId: string
    • Optional userContext: Record<string, any>

    Returns Promise<{ didConfigExist: boolean; status: "OK" }>

  • disassociateUserFromTenant(tenantId: string, recipeUserId: RecipeUserId, userContext?: Record<string, any>): Promise<{ status: "OK"; wasAssociated: boolean }>
  • getTenant(tenantId: string, userContext?: Record<string, any>): Promise<undefined | { status: "OK" } & TenantConfig>
  • listAllTenants(userContext?: Record<string, any>): Promise<{ status: "OK"; tenants: ({ tenantId: string } & TenantConfig)[] }>

Generated using TypeDoc

\ No newline at end of file +default | supertokens-node
Options
All
  • Public
  • Public/Protected
  • All
Menu

Hierarchy

  • default

Index

Constructors

Properties

init: ((config?: TypeInput) => RecipeListFunction) = Recipe.init

Type declaration

    • (config?: TypeInput): RecipeListFunction
    • Parameters

      • Optional config: TypeInput

      Returns RecipeListFunction

Methods

  • associateUserToTenant(tenantId: string, recipeUserId: RecipeUserId, userContext?: Record<string, any>): Promise<{ status: "OK"; wasAlreadyAssociated: boolean } | { status: "EMAIL_ALREADY_EXISTS_ERROR" | "UNKNOWN_USER_ID_ERROR" | "PHONE_NUMBER_ALREADY_EXISTS_ERROR" | "THIRD_PARTY_USER_ALREADY_EXISTS_ERROR" } | { reason: string; status: "ASSOCIATION_NOT_ALLOWED_ERROR" }>
  • Parameters

    • tenantId: string
    • recipeUserId: RecipeUserId
    • Optional userContext: Record<string, any>

    Returns Promise<{ status: "OK"; wasAlreadyAssociated: boolean } | { status: "EMAIL_ALREADY_EXISTS_ERROR" | "UNKNOWN_USER_ID_ERROR" | "PHONE_NUMBER_ALREADY_EXISTS_ERROR" | "THIRD_PARTY_USER_ALREADY_EXISTS_ERROR" } | { reason: string; status: "ASSOCIATION_NOT_ALLOWED_ERROR" }>

  • createOrUpdateTenant(tenantId: string, config?: { coreConfig?: {}; firstFactors?: null | string[]; requiredSecondaryFactors?: null | string[] }, userContext?: Record<string, any>): Promise<{ createdNew: boolean; status: "OK" }>
  • Parameters

    • tenantId: string
    • Optional config: { coreConfig?: {}; firstFactors?: null | string[]; requiredSecondaryFactors?: null | string[] }
      • Optional coreConfig?: {}
        • [key: string]: any
      • Optional firstFactors?: null | string[]
      • Optional requiredSecondaryFactors?: null | string[]
    • Optional userContext: Record<string, any>

    Returns Promise<{ createdNew: boolean; status: "OK" }>

  • createOrUpdateThirdPartyConfig(tenantId: string, config: ProviderConfig, skipValidation?: boolean, userContext?: Record<string, any>): Promise<{ createdNew: boolean; status: "OK" }>
  • Parameters

    • tenantId: string
    • config: ProviderConfig
    • Optional skipValidation: boolean
    • Optional userContext: Record<string, any>

    Returns Promise<{ createdNew: boolean; status: "OK" }>

  • deleteTenant(tenantId: string, userContext?: Record<string, any>): Promise<{ didExist: boolean; status: "OK" }>
  • deleteThirdPartyConfig(tenantId: string, thirdPartyId: string, userContext?: Record<string, any>): Promise<{ didConfigExist: boolean; status: "OK" }>
  • Parameters

    • tenantId: string
    • thirdPartyId: string
    • Optional userContext: Record<string, any>

    Returns Promise<{ didConfigExist: boolean; status: "OK" }>

  • disassociateUserFromTenant(tenantId: string, recipeUserId: RecipeUserId, userContext?: Record<string, any>): Promise<{ status: "OK"; wasAssociated: boolean }>
  • getTenant(tenantId: string, userContext?: Record<string, any>): Promise<undefined | { status: "OK" } & TenantConfig>
  • listAllTenants(userContext?: Record<string, any>): Promise<{ status: "OK"; tenants: ({ tenantId: string } & TenantConfig)[] }>

Generated using TypeDoc

\ No newline at end of file diff --git a/docs/classes/recipe_openid.default.html b/docs/classes/recipe_openid.default.html index 19749d08c..4aab539f7 100644 --- a/docs/classes/recipe_openid.default.html +++ b/docs/classes/recipe_openid.default.html @@ -1 +1 @@ -default | supertokens-node
Options
All
  • Public
  • Public/Protected
  • All
Menu

Hierarchy

  • default

Index

Constructors

Properties

init: ((config?: TypeInput) => RecipeListFunction) = OpenIdRecipe.init

Type declaration

    • (config?: TypeInput): RecipeListFunction
    • Parameters

      • Optional config: TypeInput

      Returns RecipeListFunction

Methods

  • createJWT(payload?: any, validitySeconds?: number, useStaticSigningKey?: boolean, userContext?: Record<string, any>): Promise<{ jwt: string; status: "OK" } | { status: "UNSUPPORTED_ALGORITHM_ERROR" }>
  • Parameters

    • Optional payload: any
    • Optional validitySeconds: number
    • Optional useStaticSigningKey: boolean
    • Optional userContext: Record<string, any>

    Returns Promise<{ jwt: string; status: "OK" } | { status: "UNSUPPORTED_ALGORITHM_ERROR" }>

  • getJWKS(userContext?: Record<string, any>): Promise<{ keys: JsonWebKey[]; validityInSeconds?: number }>
  • getOpenIdDiscoveryConfiguration(userContext?: Record<string, any>): Promise<{ issuer: string; jwks_uri: string; status: "OK" }>
  • Parameters

    • Optional userContext: Record<string, any>

    Returns Promise<{ issuer: string; jwks_uri: string; status: "OK" }>

Generated using TypeDoc

\ No newline at end of file +default | supertokens-node
Options
All
  • Public
  • Public/Protected
  • All
Menu

Hierarchy

  • default

Index

Constructors

Properties

init: ((config?: TypeInput) => RecipeListFunction) = OpenIdRecipe.init

Type declaration

    • (config?: TypeInput): RecipeListFunction
    • Parameters

      • Optional config: TypeInput

      Returns RecipeListFunction

Methods

  • createJWT(payload?: any, validitySeconds?: number, useStaticSigningKey?: boolean, userContext?: Record<string, any>): Promise<{ jwt: string; status: "OK" } | { status: "UNSUPPORTED_ALGORITHM_ERROR" }>
  • Parameters

    • Optional payload: any
    • Optional validitySeconds: number
    • Optional useStaticSigningKey: boolean
    • Optional userContext: Record<string, any>

    Returns Promise<{ jwt: string; status: "OK" } | { status: "UNSUPPORTED_ALGORITHM_ERROR" }>

  • getJWKS(userContext?: Record<string, any>): Promise<{ keys: JsonWebKey[]; validityInSeconds?: number }>
  • getOpenIdDiscoveryConfiguration(userContext?: Record<string, any>): Promise<{ issuer: string; jwks_uri: string; status: "OK" }>
  • Parameters

    • Optional userContext: Record<string, any>

    Returns Promise<{ issuer: string; jwks_uri: string; status: "OK" }>

Generated using TypeDoc

\ No newline at end of file diff --git a/docs/classes/recipe_passwordless.default.html b/docs/classes/recipe_passwordless.default.html index 4a175eaa9..b3a464776 100644 --- a/docs/classes/recipe_passwordless.default.html +++ b/docs/classes/recipe_passwordless.default.html @@ -1,14 +1,14 @@ -default | supertokens-node
Options
All
  • Public
  • Public/Protected
  • All
Menu

Hierarchy

  • default

Index

Constructors

Properties

Error: typeof default = SuperTokensError
init: ((config: TypeInput) => RecipeListFunction) = Recipe.init

Type declaration

    • (config: TypeInput): RecipeListFunction
    • Parameters

      • config: TypeInput

      Returns RecipeListFunction

Methods

  • checkCode(input: { deviceId: string; preAuthSessionId: string; tenantId: string; userContext?: Record<string, any>; userInputCode: string } | { linkCode: string; preAuthSessionId: string; tenantId: string; userContext?: Record<string, any> }): Promise<{ consumedDevice: { email?: string; failedCodeInputAttemptCount: number; phoneNumber?: string; preAuthSessionId: string }; status: "OK" } | { failedCodeInputAttemptCount: number; maximumCodeInputAttempts: number; status: "INCORRECT_USER_INPUT_CODE_ERROR" | "EXPIRED_USER_INPUT_CODE_ERROR" } | { status: "RESTART_FLOW_ERROR" }>
  • +default | supertokens-node
    Options
    All
    • Public
    • Public/Protected
    • All
    Menu

    Hierarchy

    • default

    Index

    Constructors

    Properties

    Error: typeof default = SuperTokensError
    init: ((config: TypeInput) => RecipeListFunction) = Recipe.init

    Type declaration

      • (config: TypeInput): RecipeListFunction
      • Parameters

        • config: TypeInput

        Returns RecipeListFunction

    Methods

    • checkCode(input: { deviceId: string; preAuthSessionId: string; tenantId: string; userContext?: Record<string, any>; userInputCode: string } | { linkCode: string; preAuthSessionId: string; tenantId: string; userContext?: Record<string, any> }): Promise<{ consumedDevice: { email?: string; failedCodeInputAttemptCount: number; phoneNumber?: string; preAuthSessionId: string }; status: "OK" } | { failedCodeInputAttemptCount: number; maximumCodeInputAttempts: number; status: "INCORRECT_USER_INPUT_CODE_ERROR" | "EXPIRED_USER_INPUT_CODE_ERROR" } | { status: "RESTART_FLOW_ERROR" }>
    • This function will only verify the code (not consume it), and: NOT create a new user if it doesn't exist NOT verify the user email if it exists NOT do any linking NOT delete the code unless it returned RESTART_FLOW_ERROR

      -

      Parameters

      • input: { deviceId: string; preAuthSessionId: string; tenantId: string; userContext?: Record<string, any>; userInputCode: string } | { linkCode: string; preAuthSessionId: string; tenantId: string; userContext?: Record<string, any> }

      Returns Promise<{ consumedDevice: { email?: string; failedCodeInputAttemptCount: number; phoneNumber?: string; preAuthSessionId: string }; status: "OK" } | { failedCodeInputAttemptCount: number; maximumCodeInputAttempts: number; status: "INCORRECT_USER_INPUT_CODE_ERROR" | "EXPIRED_USER_INPUT_CODE_ERROR" } | { status: "RESTART_FLOW_ERROR" }>

    • consumeCode(input: { deviceId: string; preAuthSessionId: string; session?: undefined; tenantId: string; userContext?: Record<string, any>; userInputCode: string } | { linkCode: string; preAuthSessionId: string; session?: undefined; tenantId: string; userContext?: Record<string, any> }): Promise<{ consumedDevice: { email?: string; failedCodeInputAttemptCount: number; phoneNumber?: string; preAuthSessionId: string }; createdNewRecipeUser: boolean; recipeUserId: RecipeUserId; status: "OK"; user: User } | { failedCodeInputAttemptCount: number; maximumCodeInputAttempts: number; status: "INCORRECT_USER_INPUT_CODE_ERROR" | "EXPIRED_USER_INPUT_CODE_ERROR" } | { status: "RESTART_FLOW_ERROR" }>
    • consumeCode(input: { deviceId: string; preAuthSessionId: string; session: SessionContainer; tenantId: string; userContext?: Record<string, any>; userInputCode: string } | { linkCode: string; preAuthSessionId: string; session: SessionContainer; tenantId: string; userContext?: Record<string, any> }): Promise<{ consumedDevice: { email?: string; failedCodeInputAttemptCount: number; phoneNumber?: string; preAuthSessionId: string }; createdNewRecipeUser: boolean; recipeUserId: RecipeUserId; status: "OK"; user: User } | { failedCodeInputAttemptCount: number; maximumCodeInputAttempts: number; status: "INCORRECT_USER_INPUT_CODE_ERROR" | "EXPIRED_USER_INPUT_CODE_ERROR" } | { status: "RESTART_FLOW_ERROR" } | { reason: "ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR" | "RECIPE_USER_ID_ALREADY_LINKED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR" | "EMAIL_VERIFICATION_REQUIRED" | "SESSION_USER_ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR"; status: "LINKING_TO_SESSION_USER_FAILED" }>
    • +

      Parameters

      • input: { deviceId: string; preAuthSessionId: string; tenantId: string; userContext?: Record<string, any>; userInputCode: string } | { linkCode: string; preAuthSessionId: string; tenantId: string; userContext?: Record<string, any> }

      Returns Promise<{ consumedDevice: { email?: string; failedCodeInputAttemptCount: number; phoneNumber?: string; preAuthSessionId: string }; status: "OK" } | { failedCodeInputAttemptCount: number; maximumCodeInputAttempts: number; status: "INCORRECT_USER_INPUT_CODE_ERROR" | "EXPIRED_USER_INPUT_CODE_ERROR" } | { status: "RESTART_FLOW_ERROR" }>

    • consumeCode(input: { deviceId: string; preAuthSessionId: string; session?: undefined; tenantId: string; userContext?: Record<string, any>; userInputCode: string } | { linkCode: string; preAuthSessionId: string; session?: undefined; tenantId: string; userContext?: Record<string, any> }): Promise<{ consumedDevice: { email?: string; failedCodeInputAttemptCount: number; phoneNumber?: string; preAuthSessionId: string }; createdNewRecipeUser: boolean; recipeUserId: RecipeUserId; status: "OK"; user: User } | { failedCodeInputAttemptCount: number; maximumCodeInputAttempts: number; status: "INCORRECT_USER_INPUT_CODE_ERROR" | "EXPIRED_USER_INPUT_CODE_ERROR" } | { status: "RESTART_FLOW_ERROR" }>
    • consumeCode(input: { deviceId: string; preAuthSessionId: string; session: SessionContainer; tenantId: string; userContext?: Record<string, any>; userInputCode: string } | { linkCode: string; preAuthSessionId: string; session: SessionContainer; tenantId: string; userContext?: Record<string, any> }): Promise<{ consumedDevice: { email?: string; failedCodeInputAttemptCount: number; phoneNumber?: string; preAuthSessionId: string }; createdNewRecipeUser: boolean; recipeUserId: RecipeUserId; status: "OK"; user: User } | { failedCodeInputAttemptCount: number; maximumCodeInputAttempts: number; status: "INCORRECT_USER_INPUT_CODE_ERROR" | "EXPIRED_USER_INPUT_CODE_ERROR" } | { status: "RESTART_FLOW_ERROR" } | { reason: "ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR" | "RECIPE_USER_ID_ALREADY_LINKED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR" | "EMAIL_VERIFICATION_REQUIRED" | "SESSION_USER_ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR"; status: "LINKING_TO_SESSION_USER_FAILED" }>
      1. verifies the code
      2. creates the user if it doesn't exist
      3. tries to link it
      4. marks the email as verified
      -

      Parameters

      • input: { deviceId: string; preAuthSessionId: string; session?: undefined; tenantId: string; userContext?: Record<string, any>; userInputCode: string } | { linkCode: string; preAuthSessionId: string; session?: undefined; tenantId: string; userContext?: Record<string, any> }

      Returns Promise<{ consumedDevice: { email?: string; failedCodeInputAttemptCount: number; phoneNumber?: string; preAuthSessionId: string }; createdNewRecipeUser: boolean; recipeUserId: RecipeUserId; status: "OK"; user: User } | { failedCodeInputAttemptCount: number; maximumCodeInputAttempts: number; status: "INCORRECT_USER_INPUT_CODE_ERROR" | "EXPIRED_USER_INPUT_CODE_ERROR" } | { status: "RESTART_FLOW_ERROR" }>

    • Parameters

      • input: { deviceId: string; preAuthSessionId: string; session: SessionContainer; tenantId: string; userContext?: Record<string, any>; userInputCode: string } | { linkCode: string; preAuthSessionId: string; session: SessionContainer; tenantId: string; userContext?: Record<string, any> }

      Returns Promise<{ consumedDevice: { email?: string; failedCodeInputAttemptCount: number; phoneNumber?: string; preAuthSessionId: string }; createdNewRecipeUser: boolean; recipeUserId: RecipeUserId; status: "OK"; user: User } | { failedCodeInputAttemptCount: number; maximumCodeInputAttempts: number; status: "INCORRECT_USER_INPUT_CODE_ERROR" | "EXPIRED_USER_INPUT_CODE_ERROR" } | { status: "RESTART_FLOW_ERROR" } | { reason: "ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR" | "RECIPE_USER_ID_ALREADY_LINKED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR" | "EMAIL_VERIFICATION_REQUIRED" | "SESSION_USER_ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR"; status: "LINKING_TO_SESSION_USER_FAILED" }>

    • createCode(input: { email: string } & { session?: SessionContainer; tenantId: string; userContext?: Record<string, any>; userInputCode?: string } & { phoneNumber: string } & { session?: SessionContainer; tenantId: string; userContext?: Record<string, any>; userInputCode?: string }): Promise<{ codeId: string; codeLifetime: number; deviceId: string; linkCode: string; preAuthSessionId: string; status: "OK"; timeCreated: number; userInputCode: string }>
    • Parameters

      • input: { email: string } & { session?: SessionContainer; tenantId: string; userContext?: Record<string, any>; userInputCode?: string } & { phoneNumber: string } & { session?: SessionContainer; tenantId: string; userContext?: Record<string, any>; userInputCode?: string }

      Returns Promise<{ codeId: string; codeLifetime: number; deviceId: string; linkCode: string; preAuthSessionId: string; status: "OK"; timeCreated: number; userInputCode: string }>

    • createMagicLink(input: { email: string; tenantId: string; userContext?: Record<string, any> } | { phoneNumber: string; tenantId: string; userContext?: Record<string, any> }): Promise<string>
    • Parameters

      • input: { email: string; tenantId: string; userContext?: Record<string, any> } | { phoneNumber: string; tenantId: string; userContext?: Record<string, any> }

      Returns Promise<string>

    • createNewCodeForDevice(input: { deviceId: string; tenantId: string; userContext?: Record<string, any>; userInputCode?: string }): Promise<{ codeId: string; codeLifetime: number; deviceId: string; linkCode: string; preAuthSessionId: string; status: "OK"; timeCreated: number; userInputCode: string } | { status: "RESTART_FLOW_ERROR" | "USER_INPUT_CODE_ALREADY_USED_ERROR" }>
    • Parameters

      • input: { deviceId: string; tenantId: string; userContext?: Record<string, any>; userInputCode?: string }
        • deviceId: string
        • tenantId: string
        • Optional userContext?: Record<string, any>
        • Optional userInputCode?: string

      Returns Promise<{ codeId: string; codeLifetime: number; deviceId: string; linkCode: string; preAuthSessionId: string; status: "OK"; timeCreated: number; userInputCode: string } | { status: "RESTART_FLOW_ERROR" | "USER_INPUT_CODE_ALREADY_USED_ERROR" }>

    • listCodesByDeviceId(input: { deviceId: string; tenantId: string; userContext?: Record<string, any> }): Promise<undefined | DeviceType>
    • Parameters

      • input: { deviceId: string; tenantId: string; userContext?: Record<string, any> }
        • deviceId: string
        • tenantId: string
        • Optional userContext?: Record<string, any>

      Returns Promise<undefined | DeviceType>

    • listCodesByEmail(input: { email: string; tenantId: string; userContext?: Record<string, any> }): Promise<DeviceType[]>
    • Parameters

      • input: { email: string; tenantId: string; userContext?: Record<string, any> }
        • email: string
        • tenantId: string
        • Optional userContext?: Record<string, any>

      Returns Promise<DeviceType[]>

    • listCodesByPhoneNumber(input: { phoneNumber: string; tenantId: string; userContext?: Record<string, any> }): Promise<DeviceType[]>
    • Parameters

      • input: { phoneNumber: string; tenantId: string; userContext?: Record<string, any> }
        • phoneNumber: string
        • tenantId: string
        • Optional userContext?: Record<string, any>

      Returns Promise<DeviceType[]>

    • listCodesByPreAuthSessionId(input: { preAuthSessionId: string; tenantId: string; userContext?: Record<string, any> }): Promise<undefined | DeviceType>
    • Parameters

      • input: { preAuthSessionId: string; tenantId: string; userContext?: Record<string, any> }
        • preAuthSessionId: string
        • tenantId: string
        • Optional userContext?: Record<string, any>

      Returns Promise<undefined | DeviceType>

    • revokeAllCodes(input: { email: string; tenantId: string; userContext?: Record<string, any> } | { phoneNumber: string; tenantId: string; userContext?: Record<string, any> }): Promise<{ status: "OK" }>
    • Parameters

      • input: { email: string; tenantId: string; userContext?: Record<string, any> } | { phoneNumber: string; tenantId: string; userContext?: Record<string, any> }

      Returns Promise<{ status: "OK" }>

    • revokeCode(input: { codeId: string; tenantId: string; userContext?: Record<string, any> } | { preAuthSessionId: string; tenantId: string; userContext?: Record<string, any> }): Promise<{ status: "OK" }>
    • Parameters

      • input: { codeId: string; tenantId: string; userContext?: Record<string, any> } | { preAuthSessionId: string; tenantId: string; userContext?: Record<string, any> }

      Returns Promise<{ status: "OK" }>

    • sendEmail(input: TypePasswordlessEmailDeliveryInput & { userContext?: Record<string, any> }): Promise<void>
    • sendSms(input: TypePasswordlessSmsDeliveryInput & { userContext?: Record<string, any> }): Promise<void>
    • signInUp(input: { email: string; session?: SessionContainer; tenantId: string; userContext?: Record<string, any> } | { phoneNumber: string; session?: SessionContainer; tenantId: string; userContext?: Record<string, any> }): Promise<{ createdNewRecipeUser: boolean; recipeUserId: RecipeUserId; status: string; user: User }>
    • updateUser(input: { email?: null | string; phoneNumber?: null | string; recipeUserId: RecipeUserId; userContext?: Record<string, any> }): Promise<{ status: "OK" | "EMAIL_ALREADY_EXISTS_ERROR" | "UNKNOWN_USER_ID_ERROR" | "PHONE_NUMBER_ALREADY_EXISTS_ERROR" } | { reason: string; status: "EMAIL_CHANGE_NOT_ALLOWED_ERROR" | "PHONE_NUMBER_CHANGE_NOT_ALLOWED_ERROR" }>
    • Parameters

      • input: { email?: null | string; phoneNumber?: null | string; recipeUserId: RecipeUserId; userContext?: Record<string, any> }
        • Optional email?: null | string
        • Optional phoneNumber?: null | string
        • recipeUserId: RecipeUserId
        • Optional userContext?: Record<string, any>

      Returns Promise<{ status: "OK" | "EMAIL_ALREADY_EXISTS_ERROR" | "UNKNOWN_USER_ID_ERROR" | "PHONE_NUMBER_ALREADY_EXISTS_ERROR" } | { reason: string; status: "EMAIL_CHANGE_NOT_ALLOWED_ERROR" | "PHONE_NUMBER_CHANGE_NOT_ALLOWED_ERROR" }>

    Legend

    • Variable
    • Function
    • Function with type parameter
    • Type alias
    • Type alias with type parameter
    • Class
    • Class with type parameter
    • Constructor
    • Static property
    • Static method
    • Interface

    Settings

    Theme

    Generated using TypeDoc

    \ No newline at end of file +

    Parameters

    • input: { deviceId: string; preAuthSessionId: string; session?: undefined; tenantId: string; userContext?: Record<string, any>; userInputCode: string } | { linkCode: string; preAuthSessionId: string; session?: undefined; tenantId: string; userContext?: Record<string, any> }

    Returns Promise<{ consumedDevice: { email?: string; failedCodeInputAttemptCount: number; phoneNumber?: string; preAuthSessionId: string }; createdNewRecipeUser: boolean; recipeUserId: RecipeUserId; status: "OK"; user: User } | { failedCodeInputAttemptCount: number; maximumCodeInputAttempts: number; status: "INCORRECT_USER_INPUT_CODE_ERROR" | "EXPIRED_USER_INPUT_CODE_ERROR" } | { status: "RESTART_FLOW_ERROR" }>

  • Parameters

    • input: { deviceId: string; preAuthSessionId: string; session: SessionContainer; tenantId: string; userContext?: Record<string, any>; userInputCode: string } | { linkCode: string; preAuthSessionId: string; session: SessionContainer; tenantId: string; userContext?: Record<string, any> }

    Returns Promise<{ consumedDevice: { email?: string; failedCodeInputAttemptCount: number; phoneNumber?: string; preAuthSessionId: string }; createdNewRecipeUser: boolean; recipeUserId: RecipeUserId; status: "OK"; user: User } | { failedCodeInputAttemptCount: number; maximumCodeInputAttempts: number; status: "INCORRECT_USER_INPUT_CODE_ERROR" | "EXPIRED_USER_INPUT_CODE_ERROR" } | { status: "RESTART_FLOW_ERROR" } | { reason: "ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR" | "RECIPE_USER_ID_ALREADY_LINKED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR" | "EMAIL_VERIFICATION_REQUIRED" | "SESSION_USER_ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR"; status: "LINKING_TO_SESSION_USER_FAILED" }>

  • createCode(input: { email: string } & { session?: SessionContainer; tenantId: string; userContext?: Record<string, any>; userInputCode?: string } & { phoneNumber: string } & { session?: SessionContainer; tenantId: string; userContext?: Record<string, any>; userInputCode?: string }): Promise<{ codeId: string; codeLifetime: number; deviceId: string; linkCode: string; preAuthSessionId: string; status: "OK"; timeCreated: number; userInputCode: string }>
  • Parameters

    • input: { email: string } & { session?: SessionContainer; tenantId: string; userContext?: Record<string, any>; userInputCode?: string } & { phoneNumber: string } & { session?: SessionContainer; tenantId: string; userContext?: Record<string, any>; userInputCode?: string }

    Returns Promise<{ codeId: string; codeLifetime: number; deviceId: string; linkCode: string; preAuthSessionId: string; status: "OK"; timeCreated: number; userInputCode: string }>

  • createMagicLink(input: { email: string; tenantId: string; userContext?: Record<string, any> } | { phoneNumber: string; tenantId: string; userContext?: Record<string, any> }): Promise<string>
  • Parameters

    • input: { email: string; tenantId: string; userContext?: Record<string, any> } | { phoneNumber: string; tenantId: string; userContext?: Record<string, any> }

    Returns Promise<string>

  • createNewCodeForDevice(input: { deviceId: string; tenantId: string; userContext?: Record<string, any>; userInputCode?: string }): Promise<{ codeId: string; codeLifetime: number; deviceId: string; linkCode: string; preAuthSessionId: string; status: "OK"; timeCreated: number; userInputCode: string } | { status: "RESTART_FLOW_ERROR" | "USER_INPUT_CODE_ALREADY_USED_ERROR" }>
  • Parameters

    • input: { deviceId: string; tenantId: string; userContext?: Record<string, any>; userInputCode?: string }
      • deviceId: string
      • tenantId: string
      • Optional userContext?: Record<string, any>
      • Optional userInputCode?: string

    Returns Promise<{ codeId: string; codeLifetime: number; deviceId: string; linkCode: string; preAuthSessionId: string; status: "OK"; timeCreated: number; userInputCode: string } | { status: "RESTART_FLOW_ERROR" | "USER_INPUT_CODE_ALREADY_USED_ERROR" }>

  • listCodesByDeviceId(input: { deviceId: string; tenantId: string; userContext?: Record<string, any> }): Promise<undefined | DeviceType>
  • Parameters

    • input: { deviceId: string; tenantId: string; userContext?: Record<string, any> }
      • deviceId: string
      • tenantId: string
      • Optional userContext?: Record<string, any>

    Returns Promise<undefined | DeviceType>

  • listCodesByEmail(input: { email: string; tenantId: string; userContext?: Record<string, any> }): Promise<DeviceType[]>
  • Parameters

    • input: { email: string; tenantId: string; userContext?: Record<string, any> }
      • email: string
      • tenantId: string
      • Optional userContext?: Record<string, any>

    Returns Promise<DeviceType[]>

  • listCodesByPhoneNumber(input: { phoneNumber: string; tenantId: string; userContext?: Record<string, any> }): Promise<DeviceType[]>
  • Parameters

    • input: { phoneNumber: string; tenantId: string; userContext?: Record<string, any> }
      • phoneNumber: string
      • tenantId: string
      • Optional userContext?: Record<string, any>

    Returns Promise<DeviceType[]>

  • listCodesByPreAuthSessionId(input: { preAuthSessionId: string; tenantId: string; userContext?: Record<string, any> }): Promise<undefined | DeviceType>
  • Parameters

    • input: { preAuthSessionId: string; tenantId: string; userContext?: Record<string, any> }
      • preAuthSessionId: string
      • tenantId: string
      • Optional userContext?: Record<string, any>

    Returns Promise<undefined | DeviceType>

  • revokeAllCodes(input: { email: string; tenantId: string; userContext?: Record<string, any> } | { phoneNumber: string; tenantId: string; userContext?: Record<string, any> }): Promise<{ status: "OK" }>
  • Parameters

    • input: { email: string; tenantId: string; userContext?: Record<string, any> } | { phoneNumber: string; tenantId: string; userContext?: Record<string, any> }

    Returns Promise<{ status: "OK" }>

  • revokeCode(input: { codeId: string; tenantId: string; userContext?: Record<string, any> } | { preAuthSessionId: string; tenantId: string; userContext?: Record<string, any> }): Promise<{ status: "OK" }>
  • Parameters

    • input: { codeId: string; tenantId: string; userContext?: Record<string, any> } | { preAuthSessionId: string; tenantId: string; userContext?: Record<string, any> }

    Returns Promise<{ status: "OK" }>

  • sendEmail(input: TypePasswordlessEmailDeliveryInput & { userContext?: Record<string, any> }): Promise<void>
  • sendSms(input: TypePasswordlessSmsDeliveryInput & { userContext?: Record<string, any> }): Promise<void>
  • signInUp(input: { email: string; session?: SessionContainer; tenantId: string; userContext?: Record<string, any> } | { phoneNumber: string; session?: SessionContainer; tenantId: string; userContext?: Record<string, any> }): Promise<{ createdNewRecipeUser: boolean; recipeUserId: RecipeUserId; status: string; user: User }>
  • updateUser(input: { email?: null | string; phoneNumber?: null | string; recipeUserId: RecipeUserId; userContext?: Record<string, any> }): Promise<{ status: "OK" | "EMAIL_ALREADY_EXISTS_ERROR" | "UNKNOWN_USER_ID_ERROR" | "PHONE_NUMBER_ALREADY_EXISTS_ERROR" } | { reason: string; status: "EMAIL_CHANGE_NOT_ALLOWED_ERROR" | "PHONE_NUMBER_CHANGE_NOT_ALLOWED_ERROR" }>
  • Parameters

    • input: { email?: null | string; phoneNumber?: null | string; recipeUserId: RecipeUserId; userContext?: Record<string, any> }
      • Optional email?: null | string
      • Optional phoneNumber?: null | string
      • recipeUserId: RecipeUserId
      • Optional userContext?: Record<string, any>

    Returns Promise<{ status: "OK" | "EMAIL_ALREADY_EXISTS_ERROR" | "UNKNOWN_USER_ID_ERROR" | "PHONE_NUMBER_ALREADY_EXISTS_ERROR" } | { reason: string; status: "EMAIL_CHANGE_NOT_ALLOWED_ERROR" | "PHONE_NUMBER_CHANGE_NOT_ALLOWED_ERROR" }>

Generated using TypeDoc

\ No newline at end of file diff --git a/docs/classes/recipe_session.default.html b/docs/classes/recipe_session.default.html index 5e579c72e..a51d4bfb3 100644 --- a/docs/classes/recipe_session.default.html +++ b/docs/classes/recipe_session.default.html @@ -1,4 +1,4 @@ -default | supertokens-node
Options
All
  • Public
  • Public/Protected
  • All
Menu

Hierarchy

  • default

Index

Constructors

Properties

Error: typeof default = SuperTokensError
init: ((config?: TypeInput) => RecipeListFunction) = Recipe.init

Type declaration

    • (config?: TypeInput): RecipeListFunction
    • Parameters

      • Optional config: TypeInput

      Returns RecipeListFunction

Methods

  • createJWT(payload?: any, validitySeconds?: number, useStaticSigningKey?: boolean, userContext?: Record<string, any>): Promise<{ jwt: string; status: "OK" } | { status: "UNSUPPORTED_ALGORITHM_ERROR" }>
  • Parameters

    • Optional payload: any
    • Optional validitySeconds: number
    • Optional useStaticSigningKey: boolean
    • Optional userContext: Record<string, any>

    Returns Promise<{ jwt: string; status: "OK" } | { status: "UNSUPPORTED_ALGORITHM_ERROR" }>

  • createNewSession(req: any, res: any, tenantId: string, recipeUserId: RecipeUserId, accessTokenPayload?: any, sessionDataInDatabase?: any, userContext?: Record<string, any>): Promise<SessionContainer>
  • createNewSessionWithoutRequestResponse(tenantId: string, recipeUserId: RecipeUserId, accessTokenPayload?: any, sessionDataInDatabase?: any, disableAntiCsrf?: boolean, userContext?: Record<string, any>): Promise<SessionContainer>
  • fetchAndSetClaim(sessionHandle: string, claim: SessionClaim<any>, userContext?: Record<string, any>): Promise<boolean>
  • getAllSessionHandlesForUser(userId: string, fetchSessionsForAllLinkedAccounts?: boolean, tenantId?: string, userContext?: Record<string, any>): Promise<string[]>
  • Parameters

    • userId: string
    • fetchSessionsForAllLinkedAccounts: boolean = true
    • Optional tenantId: string
    • Optional userContext: Record<string, any>

    Returns Promise<string[]>

  • getClaimValue<T>(sessionHandle: string, claim: SessionClaim<T>, userContext?: Record<string, any>): Promise<{ status: "SESSION_DOES_NOT_EXIST_ERROR" } | { status: "OK"; value: undefined | T }>
  • Type Parameters

    • T

    Parameters

    • sessionHandle: string
    • claim: SessionClaim<T>
    • Optional userContext: Record<string, any>

    Returns Promise<{ status: "SESSION_DOES_NOT_EXIST_ERROR" } | { status: "OK"; value: undefined | T }>

  • getJWKS(userContext?: Record<string, any>): Promise<{ keys: JsonWebKey[] }>
  • getOpenIdDiscoveryConfiguration(userContext?: Record<string, any>): Promise<{ issuer: string; jwks_uri: string; status: "OK" }>
  • getSessionInformation(sessionHandle: string, userContext?: Record<string, any>): Promise<undefined | SessionInformation>
  • getSessionWithoutRequestResponse(accessToken: string, antiCsrfToken?: string): Promise<SessionContainer>
  • getSessionWithoutRequestResponse(accessToken: string, antiCsrfToken?: string, options?: VerifySessionOptions & { sessionRequired?: true }, userContext?: Record<string, any>): Promise<SessionContainer>
  • getSessionWithoutRequestResponse(accessToken: string, antiCsrfToken?: string, options?: VerifySessionOptions & { sessionRequired: false }, userContext?: Record<string, any>): Promise<undefined | SessionContainer>
  • getSessionWithoutRequestResponse(accessToken: string, antiCsrfToken?: string, options?: VerifySessionOptions, userContext?: Record<string, any>): Promise<undefined | SessionContainer>
  • mergeIntoAccessTokenPayload(sessionHandle: string, accessTokenPayloadUpdate: JSONObject, userContext?: Record<string, any>): Promise<boolean>
  • refreshSession(req: any, res: any, userContext?: Record<string, any>): Promise<SessionContainer>
  • refreshSessionWithoutRequestResponse(refreshToken: string, disableAntiCsrf?: boolean, antiCsrfToken?: string, userContext?: Record<string, any>): Promise<SessionContainer>
  • removeClaim(sessionHandle: string, claim: SessionClaim<any>, userContext?: Record<string, any>): Promise<boolean>
  • revokeAllSessionsForUser(userId: string, revokeSessionsForLinkedAccounts?: boolean, tenantId?: string, userContext?: Record<string, any>): Promise<string[]>
  • Parameters

    • userId: string
    • revokeSessionsForLinkedAccounts: boolean = true
    • Optional tenantId: string
    • Optional userContext: Record<string, any>

    Returns Promise<string[]>

  • revokeMultipleSessions(sessionHandles: string[], userContext?: Record<string, any>): Promise<string[]>
  • revokeSession(sessionHandle: string, userContext?: Record<string, any>): Promise<boolean>
  • setClaimValue<T>(sessionHandle: string, claim: SessionClaim<T>, value: T, userContext?: Record<string, any>): Promise<boolean>
  • Type Parameters

    • T

    Parameters

    • sessionHandle: string
    • claim: SessionClaim<T>
    • value: T
    • Optional userContext: Record<string, any>

    Returns Promise<boolean>

  • updateSessionDataInDatabase(sessionHandle: string, newSessionData: any, userContext?: Record<string, any>): Promise<boolean>

Generated using TypeDoc

\ No newline at end of file +

Returns Promise<SessionContainer>

  • Parameters

    Returns Promise<SessionContainer>

  • Parameters

    Returns Promise<undefined | SessionContainer>

  • Parameters

    Returns Promise<undefined | SessionContainer>

  • Generated using TypeDoc

    \ No newline at end of file diff --git a/docs/classes/recipe_thirdparty.default.html b/docs/classes/recipe_thirdparty.default.html index b49c29614..bd5c0994a 100644 --- a/docs/classes/recipe_thirdparty.default.html +++ b/docs/classes/recipe_thirdparty.default.html @@ -1 +1 @@ -default | supertokens-node
    Options
    All
    • Public
    • Public/Protected
    • All
    Menu

    Hierarchy

    • default

    Index

    Constructors

    Properties

    Error: typeof default = SuperTokensError
    init: ((config?: TypeInput) => RecipeListFunction) = Recipe.init

    Type declaration

      • (config?: TypeInput): RecipeListFunction
      • Parameters

        • Optional config: TypeInput

        Returns RecipeListFunction

    Methods

    • getProvider(tenantId: string, thirdPartyId: string, clientType: undefined | string, userContext?: Record<string, any>): Promise<undefined | TypeProvider>
    • manuallyCreateOrUpdateUser(tenantId: string, thirdPartyId: string, thirdPartyUserId: string, email: string, isVerified: boolean, session?: undefined, userContext?: Record<string, any>): Promise<{ createdNewRecipeUser: boolean; recipeUserId: RecipeUserId; status: "OK"; user: User } | { reason: string; status: "EMAIL_CHANGE_NOT_ALLOWED_ERROR" } | { reason: string; status: "SIGN_IN_UP_NOT_ALLOWED" }>
    • manuallyCreateOrUpdateUser(tenantId: string, thirdPartyId: string, thirdPartyUserId: string, email: string, isVerified: boolean, session: SessionContainer, userContext?: Record<string, any>): Promise<{ createdNewRecipeUser: boolean; recipeUserId: RecipeUserId; status: "OK"; user: User } | { reason: string; status: "EMAIL_CHANGE_NOT_ALLOWED_ERROR" } | { reason: string; status: "SIGN_IN_UP_NOT_ALLOWED" } | { reason: "ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR" | "RECIPE_USER_ID_ALREADY_LINKED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR" | "EMAIL_VERIFICATION_REQUIRED" | "SESSION_USER_ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR"; status: "LINKING_TO_SESSION_USER_FAILED" }>
    • Parameters

      • tenantId: string
      • thirdPartyId: string
      • thirdPartyUserId: string
      • email: string
      • isVerified: boolean
      • Optional session: undefined
      • Optional userContext: Record<string, any>

      Returns Promise<{ createdNewRecipeUser: boolean; recipeUserId: RecipeUserId; status: "OK"; user: User } | { reason: string; status: "EMAIL_CHANGE_NOT_ALLOWED_ERROR" } | { reason: string; status: "SIGN_IN_UP_NOT_ALLOWED" }>

    • Parameters

      • tenantId: string
      • thirdPartyId: string
      • thirdPartyUserId: string
      • email: string
      • isVerified: boolean
      • session: SessionContainer
      • Optional userContext: Record<string, any>

      Returns Promise<{ createdNewRecipeUser: boolean; recipeUserId: RecipeUserId; status: "OK"; user: User } | { reason: string; status: "EMAIL_CHANGE_NOT_ALLOWED_ERROR" } | { reason: string; status: "SIGN_IN_UP_NOT_ALLOWED" } | { reason: "ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR" | "RECIPE_USER_ID_ALREADY_LINKED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR" | "EMAIL_VERIFICATION_REQUIRED" | "SESSION_USER_ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR"; status: "LINKING_TO_SESSION_USER_FAILED" }>

    Generated using TypeDoc

    \ No newline at end of file +default | supertokens-node
    Options
    All
    • Public
    • Public/Protected
    • All
    Menu

    Hierarchy

    • default

    Index

    Constructors

    Properties

    Error: typeof default = SuperTokensError
    init: ((config?: TypeInput) => RecipeListFunction) = Recipe.init

    Type declaration

      • (config?: TypeInput): RecipeListFunction
      • Parameters

        • Optional config: TypeInput

        Returns RecipeListFunction

    Methods

    • getProvider(tenantId: string, thirdPartyId: string, clientType: undefined | string, userContext?: Record<string, any>): Promise<undefined | TypeProvider>
    • manuallyCreateOrUpdateUser(tenantId: string, thirdPartyId: string, thirdPartyUserId: string, email: string, isVerified: boolean, session?: undefined, userContext?: Record<string, any>): Promise<{ createdNewRecipeUser: boolean; recipeUserId: RecipeUserId; status: "OK"; user: User } | { reason: string; status: "EMAIL_CHANGE_NOT_ALLOWED_ERROR" } | { reason: string; status: "SIGN_IN_UP_NOT_ALLOWED" }>
    • manuallyCreateOrUpdateUser(tenantId: string, thirdPartyId: string, thirdPartyUserId: string, email: string, isVerified: boolean, session: SessionContainer, userContext?: Record<string, any>): Promise<{ createdNewRecipeUser: boolean; recipeUserId: RecipeUserId; status: "OK"; user: User } | { reason: string; status: "EMAIL_CHANGE_NOT_ALLOWED_ERROR" } | { reason: string; status: "SIGN_IN_UP_NOT_ALLOWED" } | { reason: "ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR" | "RECIPE_USER_ID_ALREADY_LINKED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR" | "EMAIL_VERIFICATION_REQUIRED" | "SESSION_USER_ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR"; status: "LINKING_TO_SESSION_USER_FAILED" }>
    • Parameters

      • tenantId: string
      • thirdPartyId: string
      • thirdPartyUserId: string
      • email: string
      • isVerified: boolean
      • Optional session: undefined
      • Optional userContext: Record<string, any>

      Returns Promise<{ createdNewRecipeUser: boolean; recipeUserId: RecipeUserId; status: "OK"; user: User } | { reason: string; status: "EMAIL_CHANGE_NOT_ALLOWED_ERROR" } | { reason: string; status: "SIGN_IN_UP_NOT_ALLOWED" }>

    • Parameters

      • tenantId: string
      • thirdPartyId: string
      • thirdPartyUserId: string
      • email: string
      • isVerified: boolean
      • session: SessionContainer
      • Optional userContext: Record<string, any>

      Returns Promise<{ createdNewRecipeUser: boolean; recipeUserId: RecipeUserId; status: "OK"; user: User } | { reason: string; status: "EMAIL_CHANGE_NOT_ALLOWED_ERROR" } | { reason: string; status: "SIGN_IN_UP_NOT_ALLOWED" } | { reason: "ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR" | "RECIPE_USER_ID_ALREADY_LINKED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR" | "EMAIL_VERIFICATION_REQUIRED" | "SESSION_USER_ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR"; status: "LINKING_TO_SESSION_USER_FAILED" }>

    Generated using TypeDoc

    \ No newline at end of file diff --git a/docs/classes/recipe_totp.default.html b/docs/classes/recipe_totp.default.html index dcc0a9335..5b8523ffe 100644 --- a/docs/classes/recipe_totp.default.html +++ b/docs/classes/recipe_totp.default.html @@ -1 +1 @@ -default | supertokens-node
    Options
    All
    • Public
    • Public/Protected
    • All
    Menu

    Hierarchy

    • default

    Index

    Constructors

    Properties

    init: ((config?: TypeInput) => RecipeListFunction) = Recipe.init

    Type declaration

      • (config?: TypeInput): RecipeListFunction
      • Parameters

        • Optional config: TypeInput

        Returns RecipeListFunction

    Methods

    • createDevice(userId: string, userIdentifierInfo?: string, deviceName?: string, skew?: number, period?: number, userContext?: Record<string, any>): Promise<{ deviceName: string; qrCodeString: string; secret: string; status: "OK" } | { status: "DEVICE_ALREADY_EXISTS_ERROR" } | { status: "UNKNOWN_USER_ID_ERROR" }>
    • Parameters

      • userId: string
      • Optional userIdentifierInfo: string
      • Optional deviceName: string
      • Optional skew: number
      • Optional period: number
      • Optional userContext: Record<string, any>

      Returns Promise<{ deviceName: string; qrCodeString: string; secret: string; status: "OK" } | { status: "DEVICE_ALREADY_EXISTS_ERROR" } | { status: "UNKNOWN_USER_ID_ERROR" }>

    • listDevices(userId: string, userContext?: Record<string, any>): Promise<{ devices: { name: string; period: number; skew: number; verified: boolean }[]; status: "OK" }>
    • Parameters

      • userId: string
      • Optional userContext: Record<string, any>

      Returns Promise<{ devices: { name: string; period: number; skew: number; verified: boolean }[]; status: "OK" }>

    • removeDevice(userId: string, deviceName: string, userContext?: Record<string, any>): Promise<{ didDeviceExist: boolean; status: "OK" }>
    • Parameters

      • userId: string
      • deviceName: string
      • Optional userContext: Record<string, any>

      Returns Promise<{ didDeviceExist: boolean; status: "OK" }>

    • updateDevice(userId: string, existingDeviceName: string, newDeviceName: string, userContext?: Record<string, any>): Promise<{ status: "OK" | "DEVICE_ALREADY_EXISTS_ERROR" | "UNKNOWN_DEVICE_ERROR" }>
    • Parameters

      • userId: string
      • existingDeviceName: string
      • newDeviceName: string
      • Optional userContext: Record<string, any>

      Returns Promise<{ status: "OK" | "DEVICE_ALREADY_EXISTS_ERROR" | "UNKNOWN_DEVICE_ERROR" }>

    • verifyDevice(tenantId: string, userId: string, deviceName: string, totp: string, userContext?: Record<string, any>): Promise<{ status: "OK"; wasAlreadyVerified: boolean } | { status: "UNKNOWN_DEVICE_ERROR" } | { currentNumberOfFailedAttempts: number; maxNumberOfFailedAttempts: number; status: "INVALID_TOTP_ERROR" } | { retryAfterMs: number; status: "LIMIT_REACHED_ERROR" }>
    • Parameters

      • tenantId: string
      • userId: string
      • deviceName: string
      • totp: string
      • Optional userContext: Record<string, any>

      Returns Promise<{ status: "OK"; wasAlreadyVerified: boolean } | { status: "UNKNOWN_DEVICE_ERROR" } | { currentNumberOfFailedAttempts: number; maxNumberOfFailedAttempts: number; status: "INVALID_TOTP_ERROR" } | { retryAfterMs: number; status: "LIMIT_REACHED_ERROR" }>

    • verifyTOTP(tenantId: string, userId: string, totp: string, userContext?: Record<string, any>): Promise<{ status: "OK" | "UNKNOWN_USER_ID_ERROR" } | { currentNumberOfFailedAttempts: number; maxNumberOfFailedAttempts: number; status: "INVALID_TOTP_ERROR" } | { retryAfterMs: number; status: "LIMIT_REACHED_ERROR" }>
    • Parameters

      • tenantId: string
      • userId: string
      • totp: string
      • Optional userContext: Record<string, any>

      Returns Promise<{ status: "OK" | "UNKNOWN_USER_ID_ERROR" } | { currentNumberOfFailedAttempts: number; maxNumberOfFailedAttempts: number; status: "INVALID_TOTP_ERROR" } | { retryAfterMs: number; status: "LIMIT_REACHED_ERROR" }>

    Generated using TypeDoc

    \ No newline at end of file +default | supertokens-node
    Options
    All
    • Public
    • Public/Protected
    • All
    Menu

    Hierarchy

    • default

    Index

    Constructors

    Properties

    init: ((config?: TypeInput) => RecipeListFunction) = Recipe.init

    Type declaration

      • (config?: TypeInput): RecipeListFunction
      • Parameters

        • Optional config: TypeInput

        Returns RecipeListFunction

    Methods

    • createDevice(userId: string, userIdentifierInfo?: string, deviceName?: string, skew?: number, period?: number, userContext?: Record<string, any>): Promise<{ deviceName: string; qrCodeString: string; secret: string; status: "OK" } | { status: "DEVICE_ALREADY_EXISTS_ERROR" } | { status: "UNKNOWN_USER_ID_ERROR" }>
    • Parameters

      • userId: string
      • Optional userIdentifierInfo: string
      • Optional deviceName: string
      • Optional skew: number
      • Optional period: number
      • Optional userContext: Record<string, any>

      Returns Promise<{ deviceName: string; qrCodeString: string; secret: string; status: "OK" } | { status: "DEVICE_ALREADY_EXISTS_ERROR" } | { status: "UNKNOWN_USER_ID_ERROR" }>

    • listDevices(userId: string, userContext?: Record<string, any>): Promise<{ devices: { name: string; period: number; skew: number; verified: boolean }[]; status: "OK" }>
    • Parameters

      • userId: string
      • Optional userContext: Record<string, any>

      Returns Promise<{ devices: { name: string; period: number; skew: number; verified: boolean }[]; status: "OK" }>

    • removeDevice(userId: string, deviceName: string, userContext?: Record<string, any>): Promise<{ didDeviceExist: boolean; status: "OK" }>
    • Parameters

      • userId: string
      • deviceName: string
      • Optional userContext: Record<string, any>

      Returns Promise<{ didDeviceExist: boolean; status: "OK" }>

    • updateDevice(userId: string, existingDeviceName: string, newDeviceName: string, userContext?: Record<string, any>): Promise<{ status: "OK" | "DEVICE_ALREADY_EXISTS_ERROR" | "UNKNOWN_DEVICE_ERROR" }>
    • Parameters

      • userId: string
      • existingDeviceName: string
      • newDeviceName: string
      • Optional userContext: Record<string, any>

      Returns Promise<{ status: "OK" | "DEVICE_ALREADY_EXISTS_ERROR" | "UNKNOWN_DEVICE_ERROR" }>

    • verifyDevice(tenantId: string, userId: string, deviceName: string, totp: string, userContext?: Record<string, any>): Promise<{ status: "OK"; wasAlreadyVerified: boolean } | { status: "UNKNOWN_DEVICE_ERROR" } | { currentNumberOfFailedAttempts: number; maxNumberOfFailedAttempts: number; status: "INVALID_TOTP_ERROR" } | { retryAfterMs: number; status: "LIMIT_REACHED_ERROR" }>
    • Parameters

      • tenantId: string
      • userId: string
      • deviceName: string
      • totp: string
      • Optional userContext: Record<string, any>

      Returns Promise<{ status: "OK"; wasAlreadyVerified: boolean } | { status: "UNKNOWN_DEVICE_ERROR" } | { currentNumberOfFailedAttempts: number; maxNumberOfFailedAttempts: number; status: "INVALID_TOTP_ERROR" } | { retryAfterMs: number; status: "LIMIT_REACHED_ERROR" }>

    • verifyTOTP(tenantId: string, userId: string, totp: string, userContext?: Record<string, any>): Promise<{ status: "OK" | "UNKNOWN_USER_ID_ERROR" } | { currentNumberOfFailedAttempts: number; maxNumberOfFailedAttempts: number; status: "INVALID_TOTP_ERROR" } | { retryAfterMs: number; status: "LIMIT_REACHED_ERROR" }>
    • Parameters

      • tenantId: string
      • userId: string
      • totp: string
      • Optional userContext: Record<string, any>

      Returns Promise<{ status: "OK" | "UNKNOWN_USER_ID_ERROR" } | { currentNumberOfFailedAttempts: number; maxNumberOfFailedAttempts: number; status: "INVALID_TOTP_ERROR" } | { retryAfterMs: number; status: "LIMIT_REACHED_ERROR" }>

    Generated using TypeDoc

    \ No newline at end of file diff --git a/docs/classes/recipe_usermetadata.default.html b/docs/classes/recipe_usermetadata.default.html index 675226b49..f874813af 100644 --- a/docs/classes/recipe_usermetadata.default.html +++ b/docs/classes/recipe_usermetadata.default.html @@ -1 +1 @@ -default | supertokens-node
    Options
    All
    • Public
    • Public/Protected
    • All
    Menu

    Hierarchy

    • default

    Index

    Constructors

    Properties

    init: ((config?: TypeInput) => RecipeListFunction) = Recipe.init

    Type declaration

      • (config?: TypeInput): RecipeListFunction
      • Parameters

        • Optional config: TypeInput

        Returns RecipeListFunction

    Methods

    • clearUserMetadata(userId: string, userContext?: Record<string, any>): Promise<{ status: "OK" }>
    • getUserMetadata(userId: string, userContext?: Record<string, any>): Promise<{ metadata: any; status: "OK" }>
    • updateUserMetadata(userId: string, metadataUpdate: JSONObject, userContext?: Record<string, any>): Promise<{ metadata: JSONObject; status: "OK" }>

    Generated using TypeDoc

    \ No newline at end of file +default | supertokens-node
    Options
    All
    • Public
    • Public/Protected
    • All
    Menu

    Hierarchy

    • default

    Index

    Constructors

    Properties

    init: ((config?: TypeInput) => RecipeListFunction) = Recipe.init

    Type declaration

      • (config?: TypeInput): RecipeListFunction
      • Parameters

        • Optional config: TypeInput

        Returns RecipeListFunction

    Methods

    • clearUserMetadata(userId: string, userContext?: Record<string, any>): Promise<{ status: "OK" }>
    • getUserMetadata(userId: string, userContext?: Record<string, any>): Promise<{ metadata: any; status: "OK" }>
    • updateUserMetadata(userId: string, metadataUpdate: JSONObject, userContext?: Record<string, any>): Promise<{ metadata: JSONObject; status: "OK" }>

    Generated using TypeDoc

    \ No newline at end of file diff --git a/docs/classes/recipe_userroles.default.html b/docs/classes/recipe_userroles.default.html index c67ad58ed..eb27b0dd8 100644 --- a/docs/classes/recipe_userroles.default.html +++ b/docs/classes/recipe_userroles.default.html @@ -1 +1 @@ -default | supertokens-node
    Options
    All
    • Public
    • Public/Protected
    • All
    Menu

    Hierarchy

    • default

    Index

    Constructors

    Properties

    PermissionClaim: PermissionClaimClass = PermissionClaim
    UserRoleClaim: UserRoleClaimClass = UserRoleClaim
    init: ((config?: TypeInput) => RecipeListFunction) = Recipe.init

    Type declaration

      • (config?: TypeInput): RecipeListFunction
      • Parameters

        • Optional config: TypeInput

        Returns RecipeListFunction

    Methods

    • addRoleToUser(tenantId: string, userId: string, role: string, userContext?: Record<string, any>): Promise<{ didUserAlreadyHaveRole: boolean; status: "OK" } | { status: "UNKNOWN_ROLE_ERROR" }>
    • Parameters

      • tenantId: string
      • userId: string
      • role: string
      • Optional userContext: Record<string, any>

      Returns Promise<{ didUserAlreadyHaveRole: boolean; status: "OK" } | { status: "UNKNOWN_ROLE_ERROR" }>

    • createNewRoleOrAddPermissions(role: string, permissions: string[], userContext?: Record<string, any>): Promise<{ createdNewRole: boolean; status: "OK" }>
    • Parameters

      • role: string
      • permissions: string[]
      • Optional userContext: Record<string, any>

      Returns Promise<{ createdNewRole: boolean; status: "OK" }>

    • deleteRole(role: string, userContext?: Record<string, any>): Promise<{ didRoleExist: boolean; status: "OK" }>
    • getAllRoles(userContext?: Record<string, any>): Promise<{ roles: string[]; status: "OK" }>
    • getPermissionsForRole(role: string, userContext?: Record<string, any>): Promise<{ permissions: string[]; status: "OK" } | { status: "UNKNOWN_ROLE_ERROR" }>
    • Parameters

      • role: string
      • Optional userContext: Record<string, any>

      Returns Promise<{ permissions: string[]; status: "OK" } | { status: "UNKNOWN_ROLE_ERROR" }>

    • getRolesForUser(tenantId: string, userId: string, userContext?: Record<string, any>): Promise<{ roles: string[]; status: "OK" }>
    • Parameters

      • tenantId: string
      • userId: string
      • Optional userContext: Record<string, any>

      Returns Promise<{ roles: string[]; status: "OK" }>

    • getRolesThatHavePermission(permission: string, userContext?: Record<string, any>): Promise<{ roles: string[]; status: "OK" }>
    • getUsersThatHaveRole(tenantId: string, role: string, userContext?: Record<string, any>): Promise<{ status: "OK"; users: string[] } | { status: "UNKNOWN_ROLE_ERROR" }>
    • Parameters

      • tenantId: string
      • role: string
      • Optional userContext: Record<string, any>

      Returns Promise<{ status: "OK"; users: string[] } | { status: "UNKNOWN_ROLE_ERROR" }>

    • removePermissionsFromRole(role: string, permissions: string[], userContext?: Record<string, any>): Promise<{ status: "OK" | "UNKNOWN_ROLE_ERROR" }>
    • Parameters

      • role: string
      • permissions: string[]
      • Optional userContext: Record<string, any>

      Returns Promise<{ status: "OK" | "UNKNOWN_ROLE_ERROR" }>

    • removeUserRole(tenantId: string, userId: string, role: string, userContext?: Record<string, any>): Promise<{ didUserHaveRole: boolean; status: "OK" } | { status: "UNKNOWN_ROLE_ERROR" }>
    • Parameters

      • tenantId: string
      • userId: string
      • role: string
      • Optional userContext: Record<string, any>

      Returns Promise<{ didUserHaveRole: boolean; status: "OK" } | { status: "UNKNOWN_ROLE_ERROR" }>

    Generated using TypeDoc

    \ No newline at end of file +default | supertokens-node
    Options
    All
    • Public
    • Public/Protected
    • All
    Menu

    Hierarchy

    • default

    Index

    Constructors

    Properties

    PermissionClaim: PermissionClaimClass = PermissionClaim
    UserRoleClaim: UserRoleClaimClass = UserRoleClaim
    init: ((config?: TypeInput) => RecipeListFunction) = Recipe.init

    Type declaration

      • (config?: TypeInput): RecipeListFunction
      • Parameters

        • Optional config: TypeInput

        Returns RecipeListFunction

    Methods

    • addRoleToUser(tenantId: string, userId: string, role: string, userContext?: Record<string, any>): Promise<{ didUserAlreadyHaveRole: boolean; status: "OK" } | { status: "UNKNOWN_ROLE_ERROR" }>
    • Parameters

      • tenantId: string
      • userId: string
      • role: string
      • Optional userContext: Record<string, any>

      Returns Promise<{ didUserAlreadyHaveRole: boolean; status: "OK" } | { status: "UNKNOWN_ROLE_ERROR" }>

    • createNewRoleOrAddPermissions(role: string, permissions: string[], userContext?: Record<string, any>): Promise<{ createdNewRole: boolean; status: "OK" }>
    • Parameters

      • role: string
      • permissions: string[]
      • Optional userContext: Record<string, any>

      Returns Promise<{ createdNewRole: boolean; status: "OK" }>

    • deleteRole(role: string, userContext?: Record<string, any>): Promise<{ didRoleExist: boolean; status: "OK" }>
    • getAllRoles(userContext?: Record<string, any>): Promise<{ roles: string[]; status: "OK" }>
    • getPermissionsForRole(role: string, userContext?: Record<string, any>): Promise<{ permissions: string[]; status: "OK" } | { status: "UNKNOWN_ROLE_ERROR" }>
    • Parameters

      • role: string
      • Optional userContext: Record<string, any>

      Returns Promise<{ permissions: string[]; status: "OK" } | { status: "UNKNOWN_ROLE_ERROR" }>

    • getRolesForUser(tenantId: string, userId: string, userContext?: Record<string, any>): Promise<{ roles: string[]; status: "OK" }>
    • Parameters

      • tenantId: string
      • userId: string
      • Optional userContext: Record<string, any>

      Returns Promise<{ roles: string[]; status: "OK" }>

    • getRolesThatHavePermission(permission: string, userContext?: Record<string, any>): Promise<{ roles: string[]; status: "OK" }>
    • getUsersThatHaveRole(tenantId: string, role: string, userContext?: Record<string, any>): Promise<{ status: "OK"; users: string[] } | { status: "UNKNOWN_ROLE_ERROR" }>
    • Parameters

      • tenantId: string
      • role: string
      • Optional userContext: Record<string, any>

      Returns Promise<{ status: "OK"; users: string[] } | { status: "UNKNOWN_ROLE_ERROR" }>

    • removePermissionsFromRole(role: string, permissions: string[], userContext?: Record<string, any>): Promise<{ status: "OK" | "UNKNOWN_ROLE_ERROR" }>
    • Parameters

      • role: string
      • permissions: string[]
      • Optional userContext: Record<string, any>

      Returns Promise<{ status: "OK" | "UNKNOWN_ROLE_ERROR" }>

    • removeUserRole(tenantId: string, userId: string, role: string, userContext?: Record<string, any>): Promise<{ didUserHaveRole: boolean; status: "OK" } | { status: "UNKNOWN_ROLE_ERROR" }>
    • Parameters

      • tenantId: string
      • userId: string
      • role: string
      • Optional userContext: Record<string, any>

      Returns Promise<{ didUserHaveRole: boolean; status: "OK" } | { status: "UNKNOWN_ROLE_ERROR" }>

    Generated using TypeDoc

    \ No newline at end of file diff --git a/docs/interfaces/framework_awsLambda.SessionEvent.html b/docs/interfaces/framework_awsLambda.SessionEvent.html index 739fa12f1..25befe559 100644 --- a/docs/interfaces/framework_awsLambda.SessionEvent.html +++ b/docs/interfaces/framework_awsLambda.SessionEvent.html @@ -1 +1 @@ -SessionEvent | supertokens-node
    Options
    All
    • Public
    • Public/Protected
    • All
    Menu

    Hierarchy

    • SupertokensLambdaEvent
      • SessionEvent

    Index

    Properties

    body: null | string
    headers: APIGatewayProxyEventHeaders
    httpMethod: string
    isBase64Encoded: boolean
    multiValueHeaders: APIGatewayProxyEventMultiValueHeaders
    multiValueQueryStringParameters: null | APIGatewayProxyEventMultiValueQueryStringParameters
    path: string
    pathParameters: null | APIGatewayProxyEventPathParameters
    queryStringParameters: null | APIGatewayProxyEventQueryStringParameters
    requestContext: APIGatewayEventRequestContextWithAuthorizer<APIGatewayEventDefaultAuthorizerContext>
    resource: string
    stageVariables: null | APIGatewayProxyEventStageVariables
    supertokens: { response: { cookies: string[]; headers: { allowDuplicateKey: boolean; key: string; value: string | number | boolean }[] } }

    Type declaration

    • response: { cookies: string[]; headers: { allowDuplicateKey: boolean; key: string; value: string | number | boolean }[] }
      • cookies: string[]
      • headers: { allowDuplicateKey: boolean; key: string; value: string | number | boolean }[]

    Generated using TypeDoc

    \ No newline at end of file +SessionEvent | supertokens-node
    Options
    All
    • Public
    • Public/Protected
    • All
    Menu

    Hierarchy

    • SupertokensLambdaEvent
      • SessionEvent

    Index

    Properties

    body: null | string
    headers: APIGatewayProxyEventHeaders
    httpMethod: string
    isBase64Encoded: boolean
    multiValueHeaders: APIGatewayProxyEventMultiValueHeaders
    multiValueQueryStringParameters: null | APIGatewayProxyEventMultiValueQueryStringParameters
    path: string
    pathParameters: null | APIGatewayProxyEventPathParameters
    queryStringParameters: null | APIGatewayProxyEventQueryStringParameters
    requestContext: APIGatewayEventRequestContextWithAuthorizer<APIGatewayEventDefaultAuthorizerContext>
    resource: string
    stageVariables: null | APIGatewayProxyEventStageVariables
    supertokens: { response: { cookies: string[]; headers: { allowDuplicateKey: boolean; key: string; value: string | number | boolean }[] } }

    Type declaration

    • response: { cookies: string[]; headers: { allowDuplicateKey: boolean; key: string; value: string | number | boolean }[] }
      • cookies: string[]
      • headers: { allowDuplicateKey: boolean; key: string; value: string | number | boolean }[]

    Generated using TypeDoc

    \ No newline at end of file diff --git a/docs/interfaces/framework_awsLambda.SessionEventV2.html b/docs/interfaces/framework_awsLambda.SessionEventV2.html index 479021519..0fa6a3ca4 100644 --- a/docs/interfaces/framework_awsLambda.SessionEventV2.html +++ b/docs/interfaces/framework_awsLambda.SessionEventV2.html @@ -1 +1 @@ -SessionEventV2 | supertokens-node
    Options
    All
    • Public
    • Public/Protected
    • All
    Menu

    Hierarchy

    • SupertokensLambdaEventV2
      • SessionEventV2

    Index

    Properties

    body?: string
    cookies?: string[]
    headers: APIGatewayProxyEventHeaders
    isBase64Encoded: boolean
    pathParameters?: APIGatewayProxyEventPathParameters
    queryStringParameters?: APIGatewayProxyEventQueryStringParameters
    rawPath: string
    rawQueryString: string
    requestContext: { accountId: string; apiId: string; authorizer?: { jwt: { claims: {}; scopes: string[] } }; domainName: string; domainPrefix: string; http: { method: string; path: string; protocol: string; sourceIp: string; userAgent: string }; requestId: string; routeKey: string; stage: string; time: string; timeEpoch: number }

    Type declaration

    • accountId: string
    • apiId: string
    • Optional authorizer?: { jwt: { claims: {}; scopes: string[] } }
      • jwt: { claims: {}; scopes: string[] }
        • claims: {}
          • [name: string]: string | number | boolean | string[]
        • scopes: string[]
    • domainName: string
    • domainPrefix: string
    • http: { method: string; path: string; protocol: string; sourceIp: string; userAgent: string }
      • method: string
      • path: string
      • protocol: string
      • sourceIp: string
      • userAgent: string
    • requestId: string
    • routeKey: string
    • stage: string
    • time: string
    • timeEpoch: number
    routeKey: string
    stageVariables?: APIGatewayProxyEventStageVariables
    supertokens: { response: { cookies: string[]; headers: { allowDuplicateKey: boolean; key: string; value: string | number | boolean }[] } }

    Type declaration

    • response: { cookies: string[]; headers: { allowDuplicateKey: boolean; key: string; value: string | number | boolean }[] }
      • cookies: string[]
      • headers: { allowDuplicateKey: boolean; key: string; value: string | number | boolean }[]
    version: string

    Generated using TypeDoc

    \ No newline at end of file +SessionEventV2 | supertokens-node
    Options
    All
    • Public
    • Public/Protected
    • All
    Menu

    Hierarchy

    • SupertokensLambdaEventV2
      • SessionEventV2

    Index

    Properties

    body?: string
    cookies?: string[]
    headers: APIGatewayProxyEventHeaders
    isBase64Encoded: boolean
    pathParameters?: APIGatewayProxyEventPathParameters
    queryStringParameters?: APIGatewayProxyEventQueryStringParameters
    rawPath: string
    rawQueryString: string
    requestContext: { accountId: string; apiId: string; authorizer?: { jwt: { claims: {}; scopes: string[] } }; domainName: string; domainPrefix: string; http: { method: string; path: string; protocol: string; sourceIp: string; userAgent: string }; requestId: string; routeKey: string; stage: string; time: string; timeEpoch: number }

    Type declaration

    • accountId: string
    • apiId: string
    • Optional authorizer?: { jwt: { claims: {}; scopes: string[] } }
      • jwt: { claims: {}; scopes: string[] }
        • claims: {}
          • [name: string]: string | number | boolean | string[]
        • scopes: string[]
    • domainName: string
    • domainPrefix: string
    • http: { method: string; path: string; protocol: string; sourceIp: string; userAgent: string }
      • method: string
      • path: string
      • protocol: string
      • sourceIp: string
      • userAgent: string
    • requestId: string
    • routeKey: string
    • stage: string
    • time: string
    • timeEpoch: number
    routeKey: string
    stageVariables?: APIGatewayProxyEventStageVariables
    supertokens: { response: { cookies: string[]; headers: { allowDuplicateKey: boolean; key: string; value: string | number | boolean }[] } }

    Type declaration

    • response: { cookies: string[]; headers: { allowDuplicateKey: boolean; key: string; value: string | number | boolean }[] }
      • cookies: string[]
      • headers: { allowDuplicateKey: boolean; key: string; value: string | number | boolean }[]
    version: string

    Generated using TypeDoc

    \ No newline at end of file diff --git a/docs/interfaces/framework_express.SessionRequest.html b/docs/interfaces/framework_express.SessionRequest.html index 32a612667..06aa48d56 100644 --- a/docs/interfaces/framework_express.SessionRequest.html +++ b/docs/interfaces/framework_express.SessionRequest.html @@ -135,7 +135,7 @@
    route: any
    secure: boolean

    Short-hand for:

    req.protocol == 'https'

    -
    session?: SessionContainer
    signedCookies: any
    socket: Socket
    +
    session?: SessionContainer
    signedCookies: any
    socket: Socket

    The net.Socket object associated with the connection.

    With HTTPS support, use request.socket.getPeerCertificate() to obtain the client's authentication details.

    diff --git a/docs/interfaces/framework_hapi.SessionRequest.html b/docs/interfaces/framework_hapi.SessionRequest.html index 4a06fc25c..f49b73fb6 100644 --- a/docs/interfaces/framework_hapi.SessionRequest.html +++ b/docs/interfaces/framework_hapi.SessionRequest.html @@ -89,7 +89,7 @@
    server: Server

    Access: read only and the public server interface. The server object.

    -
    session?: SessionContainer
    state: Dictionary<any>
    +
    session?: SessionContainer
    state: Dictionary<any>

    An object containing parsed HTTP state information (cookies) where each key is the cookie name and value is the matching cookie content after processing using any registered cookie definition.

    url: URL

    The parsed request URI.

    diff --git a/docs/interfaces/framework_koa.SessionContext.html b/docs/interfaces/framework_koa.SessionContext.html index 406e848b9..9266c8230 100644 --- a/docs/interfaces/framework_koa.SessionContext.html +++ b/docs/interfaces/framework_koa.SessionContext.html @@ -81,7 +81,7 @@
    secure: boolean

    Short-hand for:

    this.protocol == 'https'

    -
    session?: SessionContainer
    socket: Socket
    +
    session?: SessionContainer
    socket: Socket

    Return the request socket.

    stale: boolean

    Check if the request is stale, aka diff --git a/docs/interfaces/framework_loopback.SessionContext.html b/docs/interfaces/framework_loopback.SessionContext.html index a0505df2a..e1e17acc9 100644 --- a/docs/interfaces/framework_loopback.SessionContext.html +++ b/docs/interfaces/framework_loopback.SessionContext.html @@ -14,7 +14,7 @@

    A flag to tell if the response is finished.

    scope: BindingScope

    Scope for binding resolution

    -
    session?: SessionContainer
    subscriptionManager: ContextSubscriptionManager
    +
    session?: SessionContainer
    subscriptionManager: ContextSubscriptionManager

    Manager for observer subscriptions

    tagIndexer: ContextTagIndexer

    Indexer for bindings by tag

    diff --git a/docs/interfaces/recipe_session.SessionContainer.html b/docs/interfaces/recipe_session.SessionContainer.html index 0a5129141..70aa42b9c 100644 --- a/docs/interfaces/recipe_session.SessionContainer.html +++ b/docs/interfaces/recipe_session.SessionContainer.html @@ -1 +1 @@ -SessionContainer | supertokens-node
    Options
    All
    • Public
    • Public/Protected
    • All
    Menu

    Hierarchy

    • SessionContainer

    Index

    Methods

    • attachToRequestResponse(reqResInfo: ReqResInfo, userContext?: Record<string, any>): void | Promise<void>
    • fetchAndSetClaim<T>(claim: SessionClaim<T>, userContext?: Record<string, any>): Promise<void>
    • getAccessToken(userContext?: Record<string, any>): string
    • getAccessTokenPayload(userContext?: Record<string, any>): any
    • getAllSessionTokensDangerously(): { accessAndFrontTokenUpdated: boolean; accessToken: string; antiCsrfToken: undefined | string; frontToken: string; refreshToken: undefined | string }
    • Returns { accessAndFrontTokenUpdated: boolean; accessToken: string; antiCsrfToken: undefined | string; frontToken: string; refreshToken: undefined | string }

      • accessAndFrontTokenUpdated: boolean
      • accessToken: string
      • antiCsrfToken: undefined | string
      • frontToken: string
      • refreshToken: undefined | string
    • getClaimValue<T>(claim: SessionClaim<T>, userContext?: Record<string, any>): Promise<undefined | T>
    • getExpiry(userContext?: Record<string, any>): Promise<number>
    • getHandle(userContext?: Record<string, any>): string
    • getRecipeUserId(userContext?: Record<string, any>): RecipeUserId
    • getSessionDataFromDatabase(userContext?: Record<string, any>): Promise<any>
    • getTenantId(userContext?: Record<string, any>): string
    • getTimeCreated(userContext?: Record<string, any>): Promise<number>
    • getUserId(userContext?: Record<string, any>): string
    • mergeIntoAccessTokenPayload(accessTokenPayloadUpdate: JSONObject, userContext?: Record<string, any>): Promise<void>
    • removeClaim(claim: SessionClaim<any>, userContext?: Record<string, any>): Promise<void>
    • revokeSession(userContext?: Record<string, any>): Promise<void>
    • setClaimValue<T>(claim: SessionClaim<T>, value: T, userContext?: Record<string, any>): Promise<void>
    • updateSessionDataInDatabase(newSessionData: any, userContext?: Record<string, any>): Promise<any>

    Legend

    • Variable
    • Function
    • Function with type parameter
    • Type alias
    • Type alias with type parameter
    • Interface
    • Method
    • Class
    • Class with type parameter

    Settings

    Theme

    Generated using TypeDoc

    \ No newline at end of file +SessionContainer | supertokens-node
    Options
    All
    • Public
    • Public/Protected
    • All
    Menu

    Hierarchy

    • SessionContainer

    Index

    Methods

    • attachToRequestResponse(reqResInfo: ReqResInfo, userContext?: Record<string, any>): void | Promise<void>
    • fetchAndSetClaim<T>(claim: SessionClaim<T>, userContext?: Record<string, any>): Promise<void>
    • getAccessToken(userContext?: Record<string, any>): string
    • getAccessTokenPayload(userContext?: Record<string, any>): any
    • getAllSessionTokensDangerously(): { accessAndFrontTokenUpdated: boolean; accessToken: string; antiCsrfToken: undefined | string; frontToken: string; refreshToken: undefined | string }
    • Returns { accessAndFrontTokenUpdated: boolean; accessToken: string; antiCsrfToken: undefined | string; frontToken: string; refreshToken: undefined | string }

      • accessAndFrontTokenUpdated: boolean
      • accessToken: string
      • antiCsrfToken: undefined | string
      • frontToken: string
      • refreshToken: undefined | string
    • getClaimValue<T>(claim: SessionClaim<T>, userContext?: Record<string, any>): Promise<undefined | T>
    • getExpiry(userContext?: Record<string, any>): Promise<number>
    • getHandle(userContext?: Record<string, any>): string
    • getRecipeUserId(userContext?: Record<string, any>): RecipeUserId
    • getSessionDataFromDatabase(userContext?: Record<string, any>): Promise<any>
    • getTenantId(userContext?: Record<string, any>): string
    • getTimeCreated(userContext?: Record<string, any>): Promise<number>
    • getUserId(userContext?: Record<string, any>): string
    • mergeIntoAccessTokenPayload(accessTokenPayloadUpdate: JSONObject, userContext?: Record<string, any>): Promise<void>
    • removeClaim(claim: SessionClaim<any>, userContext?: Record<string, any>): Promise<void>
    • revokeSession(userContext?: Record<string, any>): Promise<void>
    • setClaimValue<T>(claim: SessionClaim<T>, value: T, userContext?: Record<string, any>): Promise<void>
    • updateSessionDataInDatabase(newSessionData: any, userContext?: Record<string, any>): Promise<any>

    Legend

    • Variable
    • Function
    • Function with type parameter
    • Type alias
    • Type alias with type parameter
    • Interface
    • Method
    • Class
    • Class with type parameter

    Settings

    Theme

    Generated using TypeDoc

    \ No newline at end of file diff --git a/docs/interfaces/recipe_session.VerifySessionOptions.html b/docs/interfaces/recipe_session.VerifySessionOptions.html index aa517aeb9..0ceaaff1b 100644 --- a/docs/interfaces/recipe_session.VerifySessionOptions.html +++ b/docs/interfaces/recipe_session.VerifySessionOptions.html @@ -1 +1 @@ -VerifySessionOptions | supertokens-node
    Options
    All
    • Public
    • Public/Protected
    • All
    Menu

    Legend

    • Variable
    • Function
    • Function with type parameter
    • Type alias
    • Type alias with type parameter
    • Interface
    • Property
    • Method
    • Class
    • Class with type parameter

    Settings

    Theme

    Generated using TypeDoc

    \ No newline at end of file +VerifySessionOptions | supertokens-node
    Options
    All
    • Public
    • Public/Protected
    • All
    Menu

    Legend

    • Variable
    • Function
    • Function with type parameter
    • Type alias
    • Type alias with type parameter
    • Interface
    • Property
    • Method
    • Class
    • Class with type parameter

    Settings

    Theme

    Generated using TypeDoc

    \ No newline at end of file diff --git a/docs/modules/framework.html b/docs/modules/framework.html index b33d5ea90..1b768f60e 100644 --- a/docs/modules/framework.html +++ b/docs/modules/framework.html @@ -1 +1 @@ -framework | supertokens-node
    Options
    All
    • Public
    • Public/Protected
    • All
    Menu

    Module framework

    Index

    Variables

    awsLambda: framework/awsLambda = awsLambdaFramework
    default: { awsLambda: framework/awsLambda; express: framework/express; fastify: framework/fastify; hapi: framework/hapi; koa: framework/koa; loopback: framework/loopback }

    Type declaration

    express: framework/express = expressFramework
    fastify: framework/fastify = fastifyFramework
    hapi: framework/hapi = hapiFramework
    koa: framework/koa = koaFramework
    loopback: framework/loopback = loopbackFramework

    Legend

    • Variable
    • Function
    • Function with type parameter
    • Type alias
    • Type alias with type parameter
    • Class
    • Class with type parameter
    • Interface

    Settings

    Theme

    Generated using TypeDoc

    \ No newline at end of file +framework | supertokens-node
    Options
    All
    • Public
    • Public/Protected
    • All
    Menu

    Module framework

    Index

    Variables

    awsLambda: framework/awsLambda = awsLambdaFramework
    default: { awsLambda: framework/awsLambda; express: framework/express; fastify: framework/fastify; hapi: framework/hapi; koa: framework/koa; loopback: framework/loopback }

    Type declaration

    express: framework/express = expressFramework
    fastify: framework/fastify = fastifyFramework
    hapi: framework/hapi = hapiFramework
    koa: framework/koa = koaFramework
    loopback: framework/loopback = loopbackFramework

    Legend

    • Variable
    • Function
    • Function with type parameter
    • Type alias
    • Type alias with type parameter
    • Class
    • Class with type parameter
    • Interface

    Settings

    Theme

    Generated using TypeDoc

    \ No newline at end of file diff --git a/docs/modules/framework_awsLambda.html b/docs/modules/framework_awsLambda.html index 9b8f1c596..a8d5e9fe6 100644 --- a/docs/modules/framework_awsLambda.html +++ b/docs/modules/framework_awsLambda.html @@ -1 +1 @@ -framework/awsLambda | supertokens-node
    Options
    All
    • Public
    • Public/Protected
    • All
    Menu

    Module framework/awsLambda

    Legend

    • Variable
    • Function
    • Function with type parameter
    • Type alias
    • Type alias with type parameter
    • Class
    • Class with type parameter
    • Interface

    Settings

    Theme

    Generated using TypeDoc

    \ No newline at end of file +framework/awsLambda | supertokens-node
    Options
    All
    • Public
    • Public/Protected
    • All
    Menu

    Module framework/awsLambda

    Legend

    • Variable
    • Function
    • Function with type parameter
    • Type alias
    • Type alias with type parameter
    • Class
    • Class with type parameter
    • Interface

    Settings

    Theme

    Generated using TypeDoc

    \ No newline at end of file diff --git a/docs/modules/framework_custom.html b/docs/modules/framework_custom.html index 07e0f592d..b419ca8f5 100644 --- a/docs/modules/framework_custom.html +++ b/docs/modules/framework_custom.html @@ -1 +1 @@ -framework/custom | supertokens-node
    Options
    All
    • Public
    • Public/Protected
    • All
    Menu

    Module framework/custom

    Index

    Functions

    • middleware<OrigReqType, OrigRespType>(wrapRequest?: ((req: OrigReqType) => BaseRequest), wrapResponse?: ((req: OrigRespType) => BaseResponse)): ((request: OrigReqType, response: OrigRespType, next?: NextFunction) => Promise<{ error: undefined; handled: boolean } | { error: any; handled: undefined }>)
    • Type Parameters

      Parameters

      Returns ((request: OrigReqType, response: OrigRespType, next?: NextFunction) => Promise<{ error: undefined; handled: boolean } | { error: any; handled: undefined }>)

        • (request: OrigReqType, response: OrigRespType, next?: NextFunction): Promise<{ error: undefined; handled: boolean } | { error: any; handled: undefined }>
        • Parameters

          • request: OrigReqType
          • response: OrigRespType
          • Optional next: NextFunction

          Returns Promise<{ error: undefined; handled: boolean } | { error: any; handled: undefined }>

    Legend

    • Variable
    • Function
    • Function with type parameter
    • Type alias
    • Type alias with type parameter
    • Class
    • Class with type parameter
    • Interface

    Settings

    Theme

    Generated using TypeDoc

    \ No newline at end of file +framework/custom | supertokens-node
    Options
    All
    • Public
    • Public/Protected
    • All
    Menu

    Module framework/custom

    Index

    Functions

    • middleware<OrigReqType, OrigRespType>(wrapRequest?: ((req: OrigReqType) => BaseRequest), wrapResponse?: ((req: OrigRespType) => BaseResponse)): ((request: OrigReqType, response: OrigRespType, next?: NextFunction) => Promise<{ error: undefined; handled: boolean } | { error: any; handled: undefined }>)
    • Type Parameters

      Parameters

      Returns ((request: OrigReqType, response: OrigRespType, next?: NextFunction) => Promise<{ error: undefined; handled: boolean } | { error: any; handled: undefined }>)

        • (request: OrigReqType, response: OrigRespType, next?: NextFunction): Promise<{ error: undefined; handled: boolean } | { error: any; handled: undefined }>
        • Parameters

          • request: OrigReqType
          • response: OrigRespType
          • Optional next: NextFunction

          Returns Promise<{ error: undefined; handled: boolean } | { error: any; handled: undefined }>

    Legend

    • Variable
    • Function
    • Function with type parameter
    • Type alias
    • Type alias with type parameter
    • Class
    • Class with type parameter
    • Interface

    Settings

    Theme

    Generated using TypeDoc

    \ No newline at end of file diff --git a/docs/modules/framework_express.html b/docs/modules/framework_express.html index 924806d83..67fff866b 100644 --- a/docs/modules/framework_express.html +++ b/docs/modules/framework_express.html @@ -1 +1 @@ -framework/express | supertokens-node
    Options
    All
    • Public
    • Public/Protected
    • All
    Menu

    Module framework/express

    Index

    Functions

    • errorHandler(): ((err: any, req: Request, res: Response, next: NextFunction) => Promise<void>)
    • Returns ((err: any, req: Request, res: Response, next: NextFunction) => Promise<void>)

        • (err: any, req: Request, res: Response, next: NextFunction): Promise<void>
        • Parameters

          • err: any
          • req: Request
          • res: Response
          • next: NextFunction

          Returns Promise<void>

    • middleware(): ((req: Request, res: Response, next: NextFunction) => Promise<void>)
    • Returns ((req: Request, res: Response, next: NextFunction) => Promise<void>)

        • (req: Request, res: Response, next: NextFunction): Promise<void>
        • Parameters

          • req: Request
          • res: Response
          • next: NextFunction

          Returns Promise<void>

    Legend

    • Variable
    • Function
    • Function with type parameter
    • Type alias
    • Type alias with type parameter
    • Class
    • Class with type parameter
    • Interface

    Settings

    Theme

    Generated using TypeDoc

    \ No newline at end of file +framework/express | supertokens-node
    Options
    All
    • Public
    • Public/Protected
    • All
    Menu

    Module framework/express

    Index

    Functions

    • errorHandler(): ((err: any, req: Request, res: Response, next: NextFunction) => Promise<void>)
    • Returns ((err: any, req: Request, res: Response, next: NextFunction) => Promise<void>)

        • (err: any, req: Request, res: Response, next: NextFunction): Promise<void>
        • Parameters

          • err: any
          • req: Request
          • res: Response
          • next: NextFunction

          Returns Promise<void>

    • middleware(): ((req: Request, res: Response, next: NextFunction) => Promise<void>)
    • Returns ((req: Request, res: Response, next: NextFunction) => Promise<void>)

        • (req: Request, res: Response, next: NextFunction): Promise<void>
        • Parameters

          • req: Request
          • res: Response
          • next: NextFunction

          Returns Promise<void>

    Legend

    • Variable
    • Function
    • Function with type parameter
    • Type alias
    • Type alias with type parameter
    • Class
    • Class with type parameter
    • Interface

    Settings

    Theme

    Generated using TypeDoc

    \ No newline at end of file diff --git a/docs/modules/framework_fastify.html b/docs/modules/framework_fastify.html index 16fc29c17..40c4e59f6 100644 --- a/docs/modules/framework_fastify.html +++ b/docs/modules/framework_fastify.html @@ -1 +1 @@ -framework/fastify | supertokens-node
    Options
    All
    • Public
    • Public/Protected
    • All
    Menu

    Module framework/fastify

    Index

    Type Aliases

    SessionRequest<TRequest>: TRequest & { session?: SessionContainer }

    Type Parameters

    • TRequest extends OriginalFastifyRequest = OriginalFastifyRequest

    Functions

    • errorHandler(): ((err: any, req: FastifyRequest<RouteGenericInterface, RawServerDefault, IncomingMessage>, res: FastifyReply<RawServerDefault, IncomingMessage, ServerResponse<IncomingMessage>, RouteGenericInterface, unknown>) => Promise<void>)
    • Returns ((err: any, req: FastifyRequest<RouteGenericInterface, RawServerDefault, IncomingMessage>, res: FastifyReply<RawServerDefault, IncomingMessage, ServerResponse<IncomingMessage>, RouteGenericInterface, unknown>) => Promise<void>)

        • (err: any, req: FastifyRequest<RouteGenericInterface, RawServerDefault, IncomingMessage>, res: FastifyReply<RawServerDefault, IncomingMessage, ServerResponse<IncomingMessage>, RouteGenericInterface, unknown>): Promise<void>
        • Parameters

          • err: any
          • req: FastifyRequest<RouteGenericInterface, RawServerDefault, IncomingMessage>
          • res: FastifyReply<RawServerDefault, IncomingMessage, ServerResponse<IncomingMessage>, RouteGenericInterface, unknown>

          Returns Promise<void>

    • plugin(instance: FastifyInstance<RawServerDefault, IncomingMessage, ServerResponse<IncomingMessage>, FastifyLoggerInstance>, opts: Record<never, never>, done: ((err?: Error) => void)): void
    • Parameters

      • instance: FastifyInstance<RawServerDefault, IncomingMessage, ServerResponse<IncomingMessage>, FastifyLoggerInstance>
      • opts: Record<never, never>
      • done: ((err?: Error) => void)
          • (err?: Error): void
          • Parameters

            • Optional err: Error

            Returns void

      Returns void

    Legend

    • Variable
    • Function
    • Function with type parameter
    • Type alias
    • Type alias with type parameter
    • Class
    • Class with type parameter
    • Interface

    Settings

    Theme

    Generated using TypeDoc

    \ No newline at end of file +framework/fastify | supertokens-node
    Options
    All
    • Public
    • Public/Protected
    • All
    Menu

    Module framework/fastify

    Index

    Type Aliases

    SessionRequest<TRequest>: TRequest & { session?: SessionContainer }

    Type Parameters

    • TRequest extends OriginalFastifyRequest = OriginalFastifyRequest

    Functions

    • errorHandler(): ((err: any, req: FastifyRequest<RouteGenericInterface, RawServerDefault, IncomingMessage>, res: FastifyReply<RawServerDefault, IncomingMessage, ServerResponse<IncomingMessage>, RouteGenericInterface, unknown>) => Promise<void>)
    • Returns ((err: any, req: FastifyRequest<RouteGenericInterface, RawServerDefault, IncomingMessage>, res: FastifyReply<RawServerDefault, IncomingMessage, ServerResponse<IncomingMessage>, RouteGenericInterface, unknown>) => Promise<void>)

        • (err: any, req: FastifyRequest<RouteGenericInterface, RawServerDefault, IncomingMessage>, res: FastifyReply<RawServerDefault, IncomingMessage, ServerResponse<IncomingMessage>, RouteGenericInterface, unknown>): Promise<void>
        • Parameters

          • err: any
          • req: FastifyRequest<RouteGenericInterface, RawServerDefault, IncomingMessage>
          • res: FastifyReply<RawServerDefault, IncomingMessage, ServerResponse<IncomingMessage>, RouteGenericInterface, unknown>

          Returns Promise<void>

    • plugin(instance: FastifyInstance<RawServerDefault, IncomingMessage, ServerResponse<IncomingMessage>, FastifyLoggerInstance>, opts: Record<never, never>, done: ((err?: Error) => void)): void
    • Parameters

      • instance: FastifyInstance<RawServerDefault, IncomingMessage, ServerResponse<IncomingMessage>, FastifyLoggerInstance>
      • opts: Record<never, never>
      • done: ((err?: Error) => void)
          • (err?: Error): void
          • Parameters

            • Optional err: Error

            Returns void

      Returns void

    Legend

    • Variable
    • Function
    • Function with type parameter
    • Type alias
    • Type alias with type parameter
    • Class
    • Class with type parameter
    • Interface

    Settings

    Theme

    Generated using TypeDoc

    \ No newline at end of file diff --git a/docs/modules/framework_hapi.html b/docs/modules/framework_hapi.html index 424ec0684..47136250e 100644 --- a/docs/modules/framework_hapi.html +++ b/docs/modules/framework_hapi.html @@ -1 +1 @@ -framework/hapi | supertokens-node
    Options
    All
    • Public
    • Public/Protected
    • All
    Menu

    Module framework/hapi

    Legend

    • Variable
    • Function
    • Function with type parameter
    • Type alias
    • Type alias with type parameter
    • Class
    • Class with type parameter
    • Interface

    Settings

    Theme

    Generated using TypeDoc

    \ No newline at end of file +framework/hapi | supertokens-node
    Options
    All
    • Public
    • Public/Protected
    • All
    Menu

    Module framework/hapi

    Legend

    • Variable
    • Function
    • Function with type parameter
    • Type alias
    • Type alias with type parameter
    • Class
    • Class with type parameter
    • Interface

    Settings

    Theme

    Generated using TypeDoc

    \ No newline at end of file diff --git a/docs/modules/framework_koa.html b/docs/modules/framework_koa.html index 6b0023ef9..263f666cf 100644 --- a/docs/modules/framework_koa.html +++ b/docs/modules/framework_koa.html @@ -1 +1 @@ -framework/koa | supertokens-node
    Options
    All
    • Public
    • Public/Protected
    • All
    Menu

    Module framework/koa

    Legend

    • Variable
    • Function
    • Function with type parameter
    • Type alias
    • Type alias with type parameter
    • Class
    • Class with type parameter
    • Interface

    Settings

    Theme

    Generated using TypeDoc

    \ No newline at end of file +framework/koa | supertokens-node
    Options
    All
    • Public
    • Public/Protected
    • All
    Menu

    Module framework/koa

    Legend

    • Variable
    • Function
    • Function with type parameter
    • Type alias
    • Type alias with type parameter
    • Class
    • Class with type parameter
    • Interface

    Settings

    Theme

    Generated using TypeDoc

    \ No newline at end of file diff --git a/docs/modules/framework_loopback.html b/docs/modules/framework_loopback.html index 2631d67d0..2026f8267 100644 --- a/docs/modules/framework_loopback.html +++ b/docs/modules/framework_loopback.html @@ -1 +1 @@ -framework/loopback | supertokens-node
    Options
    All
    • Public
    • Public/Protected
    • All
    Menu

    Module framework/loopback

    Legend

    • Variable
    • Function
    • Function with type parameter
    • Type alias
    • Type alias with type parameter
    • Class
    • Class with type parameter
    • Interface

    Settings

    Theme

    Generated using TypeDoc

    \ No newline at end of file +framework/loopback | supertokens-node
    Options
    All
    • Public
    • Public/Protected
    • All
    Menu

    Module framework/loopback

    Legend

    • Variable
    • Function
    • Function with type parameter
    • Type alias
    • Type alias with type parameter
    • Class
    • Class with type parameter
    • Interface

    Settings

    Theme

    Generated using TypeDoc

    \ No newline at end of file diff --git a/docs/modules/index.html b/docs/modules/index.html index 3fbe07480..075154af8 100644 --- a/docs/modules/index.html +++ b/docs/modules/index.html @@ -1 +1 @@ -index | supertokens-node
    Options
    All
    • Public
    • Public/Protected
    • All
    Menu

    Index

    Variables

    Error: typeof default = SuperTokensWrapper.Error

    Functions

    • createUserIdMapping(input: { externalUserId: string; externalUserIdInfo?: string; force?: boolean; superTokensUserId: string; userContext?: Record<string, any> }): Promise<{ status: "OK" | "UNKNOWN_SUPERTOKENS_USER_ID_ERROR" } | { doesExternalUserIdExist: boolean; doesSuperTokensUserIdExist: boolean; status: "USER_ID_MAPPING_ALREADY_EXISTS_ERROR" }>
    • Parameters

      • input: { externalUserId: string; externalUserIdInfo?: string; force?: boolean; superTokensUserId: string; userContext?: Record<string, any> }
        • externalUserId: string
        • Optional externalUserIdInfo?: string
        • Optional force?: boolean
        • superTokensUserId: string
        • Optional userContext?: Record<string, any>

      Returns Promise<{ status: "OK" | "UNKNOWN_SUPERTOKENS_USER_ID_ERROR" } | { doesExternalUserIdExist: boolean; doesSuperTokensUserIdExist: boolean; status: "USER_ID_MAPPING_ALREADY_EXISTS_ERROR" }>

    • deleteUser(userId: string, removeAllLinkedAccounts?: boolean, userContext?: Record<string, any>): Promise<{ status: "OK" }>
    • Parameters

      • userId: string
      • removeAllLinkedAccounts: boolean = true
      • Optional userContext: Record<string, any>

      Returns Promise<{ status: "OK" }>

    • deleteUserIdMapping(input: { force?: boolean; userContext?: Record<string, any>; userId: string; userIdType?: "SUPERTOKENS" | "EXTERNAL" | "ANY" }): Promise<{ didMappingExist: boolean; status: "OK" }>
    • Parameters

      • input: { force?: boolean; userContext?: Record<string, any>; userId: string; userIdType?: "SUPERTOKENS" | "EXTERNAL" | "ANY" }
        • Optional force?: boolean
        • Optional userContext?: Record<string, any>
        • userId: string
        • Optional userIdType?: "SUPERTOKENS" | "EXTERNAL" | "ANY"

      Returns Promise<{ didMappingExist: boolean; status: "OK" }>

    • getAllCORSHeaders(): string[]
    • getRequestFromUserContext(userContext: undefined | UserContext): undefined | BaseRequest
    • getUser(userId: string, userContext?: Record<string, any>): Promise<undefined | User>
    • Parameters

      • userId: string
      • Optional userContext: Record<string, any>

      Returns Promise<undefined | User>

    • getUserCount(includeRecipeIds?: string[], tenantId?: string, userContext?: Record<string, any>): Promise<number>
    • Parameters

      • Optional includeRecipeIds: string[]
      • Optional tenantId: string
      • Optional userContext: Record<string, any>

      Returns Promise<number>

    • getUserIdMapping(input: { userContext?: Record<string, any>; userId: string; userIdType?: "SUPERTOKENS" | "EXTERNAL" | "ANY" }): Promise<{ externalUserId: string; externalUserIdInfo: undefined | string; status: "OK"; superTokensUserId: string } | { status: "UNKNOWN_MAPPING_ERROR" }>
    • Parameters

      • input: { userContext?: Record<string, any>; userId: string; userIdType?: "SUPERTOKENS" | "EXTERNAL" | "ANY" }
        • Optional userContext?: Record<string, any>
        • userId: string
        • Optional userIdType?: "SUPERTOKENS" | "EXTERNAL" | "ANY"

      Returns Promise<{ externalUserId: string; externalUserIdInfo: undefined | string; status: "OK"; superTokensUserId: string } | { status: "UNKNOWN_MAPPING_ERROR" }>

    • getUsersNewestFirst(input: { includeRecipeIds?: string[]; limit?: number; paginationToken?: string; query?: {}; tenantId: string; userContext?: Record<string, any> }): Promise<{ nextPaginationToken?: string; users: User[] }>
    • Parameters

      • input: { includeRecipeIds?: string[]; limit?: number; paginationToken?: string; query?: {}; tenantId: string; userContext?: Record<string, any> }
        • Optional includeRecipeIds?: string[]
        • Optional limit?: number
        • Optional paginationToken?: string
        • Optional query?: {}
          • [key: string]: string
        • tenantId: string
        • Optional userContext?: Record<string, any>

      Returns Promise<{ nextPaginationToken?: string; users: User[] }>

    • getUsersOldestFirst(input: { includeRecipeIds?: string[]; limit?: number; paginationToken?: string; query?: {}; tenantId: string; userContext?: Record<string, any> }): Promise<{ nextPaginationToken?: string; users: User[] }>
    • Parameters

      • input: { includeRecipeIds?: string[]; limit?: number; paginationToken?: string; query?: {}; tenantId: string; userContext?: Record<string, any> }
        • Optional includeRecipeIds?: string[]
        • Optional limit?: number
        • Optional paginationToken?: string
        • Optional query?: {}
          • [key: string]: string
        • tenantId: string
        • Optional userContext?: Record<string, any>

      Returns Promise<{ nextPaginationToken?: string; users: User[] }>

    • init(config: TypeInput): void
    • listUsersByAccountInfo(tenantId: string, accountInfo: AccountInfo, doUnionOfAccountInfo?: boolean, userContext?: Record<string, any>): Promise<User[]>
    • Parameters

      • tenantId: string
      • accountInfo: AccountInfo
      • doUnionOfAccountInfo: boolean = false
      • Optional userContext: Record<string, any>

      Returns Promise<User[]>

    • updateOrDeleteUserIdMappingInfo(input: { externalUserIdInfo?: string; userContext?: Record<string, any>; userId: string; userIdType?: "SUPERTOKENS" | "EXTERNAL" | "ANY" }): Promise<{ status: "OK" | "UNKNOWN_MAPPING_ERROR" }>
    • Parameters

      • input: { externalUserIdInfo?: string; userContext?: Record<string, any>; userId: string; userIdType?: "SUPERTOKENS" | "EXTERNAL" | "ANY" }
        • Optional externalUserIdInfo?: string
        • Optional userContext?: Record<string, any>
        • userId: string
        • Optional userIdType?: "SUPERTOKENS" | "EXTERNAL" | "ANY"

      Returns Promise<{ status: "OK" | "UNKNOWN_MAPPING_ERROR" }>

    Legend

    • Variable
    • Function
    • Function with type parameter
    • Type alias
    • Type alias with type parameter
    • Class
    • Class with type parameter
    • Interface

    Settings

    Theme

    Generated using TypeDoc

    \ No newline at end of file +index | supertokens-node
    Options
    All
    • Public
    • Public/Protected
    • All
    Menu

    Index

    Variables

    Error: typeof default = SuperTokensWrapper.Error

    Functions

    • createUserIdMapping(input: { externalUserId: string; externalUserIdInfo?: string; force?: boolean; superTokensUserId: string; userContext?: Record<string, any> }): Promise<{ status: "OK" | "UNKNOWN_SUPERTOKENS_USER_ID_ERROR" } | { doesExternalUserIdExist: boolean; doesSuperTokensUserIdExist: boolean; status: "USER_ID_MAPPING_ALREADY_EXISTS_ERROR" }>
    • Parameters

      • input: { externalUserId: string; externalUserIdInfo?: string; force?: boolean; superTokensUserId: string; userContext?: Record<string, any> }
        • externalUserId: string
        • Optional externalUserIdInfo?: string
        • Optional force?: boolean
        • superTokensUserId: string
        • Optional userContext?: Record<string, any>

      Returns Promise<{ status: "OK" | "UNKNOWN_SUPERTOKENS_USER_ID_ERROR" } | { doesExternalUserIdExist: boolean; doesSuperTokensUserIdExist: boolean; status: "USER_ID_MAPPING_ALREADY_EXISTS_ERROR" }>

    • deleteUser(userId: string, removeAllLinkedAccounts?: boolean, userContext?: Record<string, any>): Promise<{ status: "OK" }>
    • Parameters

      • userId: string
      • removeAllLinkedAccounts: boolean = true
      • Optional userContext: Record<string, any>

      Returns Promise<{ status: "OK" }>

    • deleteUserIdMapping(input: { force?: boolean; userContext?: Record<string, any>; userId: string; userIdType?: "SUPERTOKENS" | "EXTERNAL" | "ANY" }): Promise<{ didMappingExist: boolean; status: "OK" }>
    • Parameters

      • input: { force?: boolean; userContext?: Record<string, any>; userId: string; userIdType?: "SUPERTOKENS" | "EXTERNAL" | "ANY" }
        • Optional force?: boolean
        • Optional userContext?: Record<string, any>
        • userId: string
        • Optional userIdType?: "SUPERTOKENS" | "EXTERNAL" | "ANY"

      Returns Promise<{ didMappingExist: boolean; status: "OK" }>

    • getAllCORSHeaders(): string[]
    • getRequestFromUserContext(userContext: undefined | UserContext): undefined | BaseRequest
    • getUser(userId: string, userContext?: Record<string, any>): Promise<undefined | User>
    • Parameters

      • userId: string
      • Optional userContext: Record<string, any>

      Returns Promise<undefined | User>

    • getUserCount(includeRecipeIds?: string[], tenantId?: string, userContext?: Record<string, any>): Promise<number>
    • Parameters

      • Optional includeRecipeIds: string[]
      • Optional tenantId: string
      • Optional userContext: Record<string, any>

      Returns Promise<number>

    • getUserIdMapping(input: { userContext?: Record<string, any>; userId: string; userIdType?: "SUPERTOKENS" | "EXTERNAL" | "ANY" }): Promise<{ externalUserId: string; externalUserIdInfo: undefined | string; status: "OK"; superTokensUserId: string } | { status: "UNKNOWN_MAPPING_ERROR" }>
    • Parameters

      • input: { userContext?: Record<string, any>; userId: string; userIdType?: "SUPERTOKENS" | "EXTERNAL" | "ANY" }
        • Optional userContext?: Record<string, any>
        • userId: string
        • Optional userIdType?: "SUPERTOKENS" | "EXTERNAL" | "ANY"

      Returns Promise<{ externalUserId: string; externalUserIdInfo: undefined | string; status: "OK"; superTokensUserId: string } | { status: "UNKNOWN_MAPPING_ERROR" }>

    • getUsersNewestFirst(input: { includeRecipeIds?: string[]; limit?: number; paginationToken?: string; query?: {}; tenantId: string; userContext?: Record<string, any> }): Promise<{ nextPaginationToken?: string; users: User[] }>
    • Parameters

      • input: { includeRecipeIds?: string[]; limit?: number; paginationToken?: string; query?: {}; tenantId: string; userContext?: Record<string, any> }
        • Optional includeRecipeIds?: string[]
        • Optional limit?: number
        • Optional paginationToken?: string
        • Optional query?: {}
          • [key: string]: string
        • tenantId: string
        • Optional userContext?: Record<string, any>

      Returns Promise<{ nextPaginationToken?: string; users: User[] }>

    • getUsersOldestFirst(input: { includeRecipeIds?: string[]; limit?: number; paginationToken?: string; query?: {}; tenantId: string; userContext?: Record<string, any> }): Promise<{ nextPaginationToken?: string; users: User[] }>
    • Parameters

      • input: { includeRecipeIds?: string[]; limit?: number; paginationToken?: string; query?: {}; tenantId: string; userContext?: Record<string, any> }
        • Optional includeRecipeIds?: string[]
        • Optional limit?: number
        • Optional paginationToken?: string
        • Optional query?: {}
          • [key: string]: string
        • tenantId: string
        • Optional userContext?: Record<string, any>

      Returns Promise<{ nextPaginationToken?: string; users: User[] }>

    • init(config: TypeInput): void
    • listUsersByAccountInfo(tenantId: string, accountInfo: AccountInfo, doUnionOfAccountInfo?: boolean, userContext?: Record<string, any>): Promise<User[]>
    • Parameters

      • tenantId: string
      • accountInfo: AccountInfo
      • doUnionOfAccountInfo: boolean = false
      • Optional userContext: Record<string, any>

      Returns Promise<User[]>

    • updateOrDeleteUserIdMappingInfo(input: { externalUserIdInfo?: string; userContext?: Record<string, any>; userId: string; userIdType?: "SUPERTOKENS" | "EXTERNAL" | "ANY" }): Promise<{ status: "OK" | "UNKNOWN_MAPPING_ERROR" }>
    • Parameters

      • input: { externalUserIdInfo?: string; userContext?: Record<string, any>; userId: string; userIdType?: "SUPERTOKENS" | "EXTERNAL" | "ANY" }
        • Optional externalUserIdInfo?: string
        • Optional userContext?: Record<string, any>
        • userId: string
        • Optional userIdType?: "SUPERTOKENS" | "EXTERNAL" | "ANY"

      Returns Promise<{ status: "OK" | "UNKNOWN_MAPPING_ERROR" }>

    Legend

    • Variable
    • Function
    • Function with type parameter
    • Type alias
    • Type alias with type parameter
    • Class
    • Class with type parameter
    • Interface

    Settings

    Theme

    Generated using TypeDoc

    \ No newline at end of file diff --git a/docs/modules/recipe_accountlinking.html b/docs/modules/recipe_accountlinking.html index b21f3b022..7b6739e27 100644 --- a/docs/modules/recipe_accountlinking.html +++ b/docs/modules/recipe_accountlinking.html @@ -1 +1 @@ -recipe/accountlinking | supertokens-node
    Options
    All
    • Public
    • Public/Protected
    • All
    Menu

    Module recipe/accountlinking

    Index

    Type Aliases

    RecipeInterface: { canCreatePrimaryUser: any; canLinkAccounts: any; createPrimaryUser: any; deleteUser: any; getUser: any; getUsers: any; linkAccounts: any; listUsersByAccountInfo: any; unlinkAccount: any }

    Type declaration

    • canCreatePrimaryUser:function
      • canCreatePrimaryUser(input: { recipeUserId: RecipeUserId; userContext: UserContext }): Promise<{ status: "OK"; wasAlreadyAPrimaryUser: boolean } | { description: string; primaryUserId: string; status: "RECIPE_USER_ID_ALREADY_LINKED_WITH_PRIMARY_USER_ID_ERROR" | "ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR" }>
      • Parameters

        Returns Promise<{ status: "OK"; wasAlreadyAPrimaryUser: boolean } | { description: string; primaryUserId: string; status: "RECIPE_USER_ID_ALREADY_LINKED_WITH_PRIMARY_USER_ID_ERROR" | "ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR" }>

    • canLinkAccounts:function
      • canLinkAccounts(input: { primaryUserId: string; recipeUserId: RecipeUserId; userContext: UserContext }): Promise<{ accountsAlreadyLinked: boolean; status: "OK" } | { description: string; primaryUserId: string; status: "RECIPE_USER_ID_ALREADY_LINKED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR" } | { description: string; primaryUserId: string; status: "ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR" } | { status: "INPUT_USER_IS_NOT_A_PRIMARY_USER" }>
      • Parameters

        • input: { primaryUserId: string; recipeUserId: RecipeUserId; userContext: UserContext }
          • primaryUserId: string
          • recipeUserId: RecipeUserId
          • userContext: UserContext

        Returns Promise<{ accountsAlreadyLinked: boolean; status: "OK" } | { description: string; primaryUserId: string; status: "RECIPE_USER_ID_ALREADY_LINKED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR" } | { description: string; primaryUserId: string; status: "ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR" } | { status: "INPUT_USER_IS_NOT_A_PRIMARY_USER" }>

    • createPrimaryUser:function
      • createPrimaryUser(input: { recipeUserId: RecipeUserId; userContext: UserContext }): Promise<{ status: "OK"; user: User; wasAlreadyAPrimaryUser: boolean } | { primaryUserId: string; status: "RECIPE_USER_ID_ALREADY_LINKED_WITH_PRIMARY_USER_ID_ERROR" } | { description: string; primaryUserId: string; status: "ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR" }>
      • Parameters

        Returns Promise<{ status: "OK"; user: User; wasAlreadyAPrimaryUser: boolean } | { primaryUserId: string; status: "RECIPE_USER_ID_ALREADY_LINKED_WITH_PRIMARY_USER_ID_ERROR" } | { description: string; primaryUserId: string; status: "ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR" }>

    • deleteUser:function
      • deleteUser(input: { removeAllLinkedAccounts: boolean; userContext: UserContext; userId: string }): Promise<{ status: "OK" }>
      • Parameters

        • input: { removeAllLinkedAccounts: boolean; userContext: UserContext; userId: string }
          • removeAllLinkedAccounts: boolean
          • userContext: UserContext
          • userId: string

        Returns Promise<{ status: "OK" }>

    • getUser:function
      • getUser(input: { userContext: UserContext; userId: string }): Promise<undefined | User>
    • getUsers:function
      • getUsers(input: { includeRecipeIds?: string[]; limit?: number; paginationToken?: string; query?: {}; tenantId: string; timeJoinedOrder: "ASC" | "DESC"; userContext: UserContext }): Promise<{ nextPaginationToken?: string; users: User[] }>
      • Parameters

        • input: { includeRecipeIds?: string[]; limit?: number; paginationToken?: string; query?: {}; tenantId: string; timeJoinedOrder: "ASC" | "DESC"; userContext: UserContext }
          • Optional includeRecipeIds?: string[]
          • Optional limit?: number
          • Optional paginationToken?: string
          • Optional query?: {}
            • [key: string]: string
          • tenantId: string
          • timeJoinedOrder: "ASC" | "DESC"
          • userContext: UserContext

        Returns Promise<{ nextPaginationToken?: string; users: User[] }>

    • linkAccounts:function
      • linkAccounts(input: { primaryUserId: string; recipeUserId: RecipeUserId; userContext: UserContext }): Promise<{ accountsAlreadyLinked: boolean; status: "OK"; user: User } | { primaryUserId: string; status: "RECIPE_USER_ID_ALREADY_LINKED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR"; user: User } | { description: string; primaryUserId: string; status: "ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR" } | { status: "INPUT_USER_IS_NOT_A_PRIMARY_USER" }>
      • Parameters

        • input: { primaryUserId: string; recipeUserId: RecipeUserId; userContext: UserContext }
          • primaryUserId: string
          • recipeUserId: RecipeUserId
          • userContext: UserContext

        Returns Promise<{ accountsAlreadyLinked: boolean; status: "OK"; user: User } | { primaryUserId: string; status: "RECIPE_USER_ID_ALREADY_LINKED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR"; user: User } | { description: string; primaryUserId: string; status: "ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR" } | { status: "INPUT_USER_IS_NOT_A_PRIMARY_USER" }>

    • listUsersByAccountInfo:function
      • listUsersByAccountInfo(input: { accountInfo: AccountInfo; doUnionOfAccountInfo: boolean; tenantId: string; userContext: UserContext }): Promise<User[]>
      • Parameters

        • input: { accountInfo: AccountInfo; doUnionOfAccountInfo: boolean; tenantId: string; userContext: UserContext }
          • accountInfo: AccountInfo
          • doUnionOfAccountInfo: boolean
          • tenantId: string
          • userContext: UserContext

        Returns Promise<User[]>

    • unlinkAccount:function
      • unlinkAccount(input: { recipeUserId: RecipeUserId; userContext: UserContext }): Promise<{ status: "OK"; wasLinked: boolean; wasRecipeUserDeleted: boolean }>

    Functions

    • canCreatePrimaryUser(recipeUserId: RecipeUserId, userContext?: Record<string, any>): Promise<{ status: "OK"; wasAlreadyAPrimaryUser: boolean } | { description: string; primaryUserId: string; status: "RECIPE_USER_ID_ALREADY_LINKED_WITH_PRIMARY_USER_ID_ERROR" | "ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR" }>
    • Parameters

      • recipeUserId: RecipeUserId
      • Optional userContext: Record<string, any>

      Returns Promise<{ status: "OK"; wasAlreadyAPrimaryUser: boolean } | { description: string; primaryUserId: string; status: "RECIPE_USER_ID_ALREADY_LINKED_WITH_PRIMARY_USER_ID_ERROR" | "ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR" }>

    • canLinkAccounts(recipeUserId: RecipeUserId, primaryUserId: string, userContext?: Record<string, any>): Promise<{ accountsAlreadyLinked: boolean; status: "OK" } | { description: string; primaryUserId: string; status: "RECIPE_USER_ID_ALREADY_LINKED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR" } | { description: string; primaryUserId: string; status: "ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR" } | { status: "INPUT_USER_IS_NOT_A_PRIMARY_USER" }>
    • Parameters

      • recipeUserId: RecipeUserId
      • primaryUserId: string
      • Optional userContext: Record<string, any>

      Returns Promise<{ accountsAlreadyLinked: boolean; status: "OK" } | { description: string; primaryUserId: string; status: "RECIPE_USER_ID_ALREADY_LINKED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR" } | { description: string; primaryUserId: string; status: "ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR" } | { status: "INPUT_USER_IS_NOT_A_PRIMARY_USER" }>

    • createPrimaryUser(recipeUserId: RecipeUserId, userContext?: Record<string, any>): Promise<{ status: "OK"; user: User; wasAlreadyAPrimaryUser: boolean } | { primaryUserId: string; status: "RECIPE_USER_ID_ALREADY_LINKED_WITH_PRIMARY_USER_ID_ERROR" } | { description: string; primaryUserId: string; status: "ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR" }>
    • Parameters

      • recipeUserId: RecipeUserId
      • Optional userContext: Record<string, any>

      Returns Promise<{ status: "OK"; user: User; wasAlreadyAPrimaryUser: boolean } | { primaryUserId: string; status: "RECIPE_USER_ID_ALREADY_LINKED_WITH_PRIMARY_USER_ID_ERROR" } | { description: string; primaryUserId: string; status: "ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR" }>

    • createPrimaryUserIdOrLinkAccounts(tenantId: string, recipeUserId: RecipeUserId, session?: SessionContainer, userContext?: Record<string, any>): Promise<User>
    • getPrimaryUserThatCanBeLinkedToRecipeUserId(tenantId: string, recipeUserId: RecipeUserId, userContext?: Record<string, any>): Promise<undefined | User>
    • init(config?: TypeInput): RecipeListFunction
    • isEmailChangeAllowed(recipeUserId: RecipeUserId, newEmail: string, isVerified: boolean, session?: SessionContainer, userContext?: Record<string, any>): Promise<boolean>
    • isSignInAllowed(tenantId: string, recipeUserId: RecipeUserId, session?: SessionContainer, userContext?: Record<string, any>): Promise<boolean>
    • isSignUpAllowed(tenantId: string, newUser: AccountInfoWithRecipeId, isVerified: boolean, session?: SessionContainer, userContext?: Record<string, any>): Promise<boolean>
    • linkAccounts(recipeUserId: RecipeUserId, primaryUserId: string, userContext?: Record<string, any>): Promise<{ accountsAlreadyLinked: boolean; status: "OK"; user: User } | { primaryUserId: string; status: "RECIPE_USER_ID_ALREADY_LINKED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR"; user: User } | { description: string; primaryUserId: string; status: "ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR" } | { status: "INPUT_USER_IS_NOT_A_PRIMARY_USER" }>
    • Parameters

      • recipeUserId: RecipeUserId
      • primaryUserId: string
      • Optional userContext: Record<string, any>

      Returns Promise<{ accountsAlreadyLinked: boolean; status: "OK"; user: User } | { primaryUserId: string; status: "RECIPE_USER_ID_ALREADY_LINKED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR"; user: User } | { description: string; primaryUserId: string; status: "ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR" } | { status: "INPUT_USER_IS_NOT_A_PRIMARY_USER" }>

    • unlinkAccount(recipeUserId: RecipeUserId, userContext?: Record<string, any>): Promise<{ status: "OK"; wasLinked: boolean; wasRecipeUserDeleted: boolean }>

    Legend

    • Variable
    • Function
    • Function with type parameter
    • Type alias
    • Type alias with type parameter
    • Class
    • Class with type parameter
    • Interface

    Settings

    Theme

    Generated using TypeDoc

    \ No newline at end of file +recipe/accountlinking | supertokens-node
    Options
    All
    • Public
    • Public/Protected
    • All
    Menu

    Module recipe/accountlinking

    Index

    Type Aliases

    RecipeInterface: { canCreatePrimaryUser: any; canLinkAccounts: any; createPrimaryUser: any; deleteUser: any; getUser: any; getUsers: any; linkAccounts: any; listUsersByAccountInfo: any; unlinkAccount: any }

    Type declaration

    • canCreatePrimaryUser:function
      • canCreatePrimaryUser(input: { recipeUserId: RecipeUserId; userContext: UserContext }): Promise<{ status: "OK"; wasAlreadyAPrimaryUser: boolean } | { description: string; primaryUserId: string; status: "RECIPE_USER_ID_ALREADY_LINKED_WITH_PRIMARY_USER_ID_ERROR" | "ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR" }>
      • Parameters

        Returns Promise<{ status: "OK"; wasAlreadyAPrimaryUser: boolean } | { description: string; primaryUserId: string; status: "RECIPE_USER_ID_ALREADY_LINKED_WITH_PRIMARY_USER_ID_ERROR" | "ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR" }>

    • canLinkAccounts:function
      • canLinkAccounts(input: { primaryUserId: string; recipeUserId: RecipeUserId; userContext: UserContext }): Promise<{ accountsAlreadyLinked: boolean; status: "OK" } | { description: string; primaryUserId: string; status: "RECIPE_USER_ID_ALREADY_LINKED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR" } | { description: string; primaryUserId: string; status: "ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR" } | { status: "INPUT_USER_IS_NOT_A_PRIMARY_USER" }>
      • Parameters

        • input: { primaryUserId: string; recipeUserId: RecipeUserId; userContext: UserContext }
          • primaryUserId: string
          • recipeUserId: RecipeUserId
          • userContext: UserContext

        Returns Promise<{ accountsAlreadyLinked: boolean; status: "OK" } | { description: string; primaryUserId: string; status: "RECIPE_USER_ID_ALREADY_LINKED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR" } | { description: string; primaryUserId: string; status: "ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR" } | { status: "INPUT_USER_IS_NOT_A_PRIMARY_USER" }>

    • createPrimaryUser:function
      • createPrimaryUser(input: { recipeUserId: RecipeUserId; userContext: UserContext }): Promise<{ status: "OK"; user: User; wasAlreadyAPrimaryUser: boolean } | { primaryUserId: string; status: "RECIPE_USER_ID_ALREADY_LINKED_WITH_PRIMARY_USER_ID_ERROR" } | { description: string; primaryUserId: string; status: "ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR" }>
      • Parameters

        Returns Promise<{ status: "OK"; user: User; wasAlreadyAPrimaryUser: boolean } | { primaryUserId: string; status: "RECIPE_USER_ID_ALREADY_LINKED_WITH_PRIMARY_USER_ID_ERROR" } | { description: string; primaryUserId: string; status: "ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR" }>

    • deleteUser:function
      • deleteUser(input: { removeAllLinkedAccounts: boolean; userContext: UserContext; userId: string }): Promise<{ status: "OK" }>
      • Parameters

        • input: { removeAllLinkedAccounts: boolean; userContext: UserContext; userId: string }
          • removeAllLinkedAccounts: boolean
          • userContext: UserContext
          • userId: string

        Returns Promise<{ status: "OK" }>

    • getUser:function
      • getUser(input: { userContext: UserContext; userId: string }): Promise<undefined | User>
    • getUsers:function
      • getUsers(input: { includeRecipeIds?: string[]; limit?: number; paginationToken?: string; query?: {}; tenantId: string; timeJoinedOrder: "ASC" | "DESC"; userContext: UserContext }): Promise<{ nextPaginationToken?: string; users: User[] }>
      • Parameters

        • input: { includeRecipeIds?: string[]; limit?: number; paginationToken?: string; query?: {}; tenantId: string; timeJoinedOrder: "ASC" | "DESC"; userContext: UserContext }
          • Optional includeRecipeIds?: string[]
          • Optional limit?: number
          • Optional paginationToken?: string
          • Optional query?: {}
            • [key: string]: string
          • tenantId: string
          • timeJoinedOrder: "ASC" | "DESC"
          • userContext: UserContext

        Returns Promise<{ nextPaginationToken?: string; users: User[] }>

    • linkAccounts:function
      • linkAccounts(input: { primaryUserId: string; recipeUserId: RecipeUserId; userContext: UserContext }): Promise<{ accountsAlreadyLinked: boolean; status: "OK"; user: User } | { primaryUserId: string; status: "RECIPE_USER_ID_ALREADY_LINKED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR"; user: User } | { description: string; primaryUserId: string; status: "ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR" } | { status: "INPUT_USER_IS_NOT_A_PRIMARY_USER" }>
      • Parameters

        • input: { primaryUserId: string; recipeUserId: RecipeUserId; userContext: UserContext }
          • primaryUserId: string
          • recipeUserId: RecipeUserId
          • userContext: UserContext

        Returns Promise<{ accountsAlreadyLinked: boolean; status: "OK"; user: User } | { primaryUserId: string; status: "RECIPE_USER_ID_ALREADY_LINKED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR"; user: User } | { description: string; primaryUserId: string; status: "ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR" } | { status: "INPUT_USER_IS_NOT_A_PRIMARY_USER" }>

    • listUsersByAccountInfo:function
      • listUsersByAccountInfo(input: { accountInfo: AccountInfo; doUnionOfAccountInfo: boolean; tenantId: string; userContext: UserContext }): Promise<User[]>
      • Parameters

        • input: { accountInfo: AccountInfo; doUnionOfAccountInfo: boolean; tenantId: string; userContext: UserContext }
          • accountInfo: AccountInfo
          • doUnionOfAccountInfo: boolean
          • tenantId: string
          • userContext: UserContext

        Returns Promise<User[]>

    • unlinkAccount:function
      • unlinkAccount(input: { recipeUserId: RecipeUserId; userContext: UserContext }): Promise<{ status: "OK"; wasLinked: boolean; wasRecipeUserDeleted: boolean }>

    Functions

    • canCreatePrimaryUser(recipeUserId: RecipeUserId, userContext?: Record<string, any>): Promise<{ status: "OK"; wasAlreadyAPrimaryUser: boolean } | { description: string; primaryUserId: string; status: "RECIPE_USER_ID_ALREADY_LINKED_WITH_PRIMARY_USER_ID_ERROR" | "ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR" }>
    • Parameters

      • recipeUserId: RecipeUserId
      • Optional userContext: Record<string, any>

      Returns Promise<{ status: "OK"; wasAlreadyAPrimaryUser: boolean } | { description: string; primaryUserId: string; status: "RECIPE_USER_ID_ALREADY_LINKED_WITH_PRIMARY_USER_ID_ERROR" | "ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR" }>

    • canLinkAccounts(recipeUserId: RecipeUserId, primaryUserId: string, userContext?: Record<string, any>): Promise<{ accountsAlreadyLinked: boolean; status: "OK" } | { description: string; primaryUserId: string; status: "RECIPE_USER_ID_ALREADY_LINKED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR" } | { description: string; primaryUserId: string; status: "ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR" } | { status: "INPUT_USER_IS_NOT_A_PRIMARY_USER" }>
    • Parameters

      • recipeUserId: RecipeUserId
      • primaryUserId: string
      • Optional userContext: Record<string, any>

      Returns Promise<{ accountsAlreadyLinked: boolean; status: "OK" } | { description: string; primaryUserId: string; status: "RECIPE_USER_ID_ALREADY_LINKED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR" } | { description: string; primaryUserId: string; status: "ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR" } | { status: "INPUT_USER_IS_NOT_A_PRIMARY_USER" }>

    • createPrimaryUser(recipeUserId: RecipeUserId, userContext?: Record<string, any>): Promise<{ status: "OK"; user: User; wasAlreadyAPrimaryUser: boolean } | { primaryUserId: string; status: "RECIPE_USER_ID_ALREADY_LINKED_WITH_PRIMARY_USER_ID_ERROR" } | { description: string; primaryUserId: string; status: "ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR" }>
    • Parameters

      • recipeUserId: RecipeUserId
      • Optional userContext: Record<string, any>

      Returns Promise<{ status: "OK"; user: User; wasAlreadyAPrimaryUser: boolean } | { primaryUserId: string; status: "RECIPE_USER_ID_ALREADY_LINKED_WITH_PRIMARY_USER_ID_ERROR" } | { description: string; primaryUserId: string; status: "ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR" }>

    • createPrimaryUserIdOrLinkAccounts(tenantId: string, recipeUserId: RecipeUserId, session?: SessionContainer, userContext?: Record<string, any>): Promise<User>
    • getPrimaryUserThatCanBeLinkedToRecipeUserId(tenantId: string, recipeUserId: RecipeUserId, userContext?: Record<string, any>): Promise<undefined | User>
    • init(config?: TypeInput): RecipeListFunction
    • isEmailChangeAllowed(recipeUserId: RecipeUserId, newEmail: string, isVerified: boolean, session?: SessionContainer, userContext?: Record<string, any>): Promise<boolean>
    • isSignInAllowed(tenantId: string, recipeUserId: RecipeUserId, session?: SessionContainer, userContext?: Record<string, any>): Promise<boolean>
    • isSignUpAllowed(tenantId: string, newUser: AccountInfoWithRecipeId, isVerified: boolean, session?: SessionContainer, userContext?: Record<string, any>): Promise<boolean>
    • linkAccounts(recipeUserId: RecipeUserId, primaryUserId: string, userContext?: Record<string, any>): Promise<{ accountsAlreadyLinked: boolean; status: "OK"; user: User } | { primaryUserId: string; status: "RECIPE_USER_ID_ALREADY_LINKED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR"; user: User } | { description: string; primaryUserId: string; status: "ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR" } | { status: "INPUT_USER_IS_NOT_A_PRIMARY_USER" }>
    • Parameters

      • recipeUserId: RecipeUserId
      • primaryUserId: string
      • Optional userContext: Record<string, any>

      Returns Promise<{ accountsAlreadyLinked: boolean; status: "OK"; user: User } | { primaryUserId: string; status: "RECIPE_USER_ID_ALREADY_LINKED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR"; user: User } | { description: string; primaryUserId: string; status: "ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR" } | { status: "INPUT_USER_IS_NOT_A_PRIMARY_USER" }>

    • unlinkAccount(recipeUserId: RecipeUserId, userContext?: Record<string, any>): Promise<{ status: "OK"; wasLinked: boolean; wasRecipeUserDeleted: boolean }>

    Legend

    • Variable
    • Function
    • Function with type parameter
    • Type alias
    • Type alias with type parameter
    • Class
    • Class with type parameter
    • Interface

    Settings

    Theme

    Generated using TypeDoc

    \ No newline at end of file diff --git a/docs/modules/recipe_dashboard.html b/docs/modules/recipe_dashboard.html index 268770d55..97e81553d 100644 --- a/docs/modules/recipe_dashboard.html +++ b/docs/modules/recipe_dashboard.html @@ -1 +1 @@ -recipe/dashboard | supertokens-node
    Options
    All
    • Public
    • Public/Protected
    • All
    Menu

    Module recipe/dashboard

    Index

    Type Aliases

    APIInterface: { dashboardGET: undefined | ((input: { options: APIOptions; userContext: UserContext }) => Promise<string>) }

    Type declaration

    • dashboardGET: undefined | ((input: { options: APIOptions; userContext: UserContext }) => Promise<string>)
    APIOptions: { appInfo: NormalisedAppinfo; config: TypeNormalisedInput; isInServerlessEnv: boolean; recipeId: string; recipeImplementation: RecipeInterface; req: BaseRequest; res: BaseResponse }

    Type declaration

    RecipeInterface: { getDashboardBundleLocation: any; shouldAllowAccess: any }

    Type declaration

    • getDashboardBundleLocation:function
      • getDashboardBundleLocation(input: { userContext: UserContext }): Promise<string>
    • shouldAllowAccess:function
      • shouldAllowAccess(input: { config: TypeNormalisedInput; req: BaseRequest; userContext: UserContext }): Promise<boolean>

    Functions

    • init(config?: TypeInput): RecipeListFunction

    Legend

    • Variable
    • Function
    • Function with type parameter
    • Type alias
    • Type alias with type parameter
    • Class
    • Class with type parameter
    • Interface

    Settings

    Theme

    Generated using TypeDoc

    \ No newline at end of file +recipe/dashboard | supertokens-node
    Options
    All
    • Public
    • Public/Protected
    • All
    Menu

    Module recipe/dashboard

    Index

    Type Aliases

    APIInterface: { dashboardGET: undefined | ((input: { options: APIOptions; userContext: UserContext }) => Promise<string>) }

    Type declaration

    • dashboardGET: undefined | ((input: { options: APIOptions; userContext: UserContext }) => Promise<string>)
    APIOptions: { appInfo: NormalisedAppinfo; config: TypeNormalisedInput; isInServerlessEnv: boolean; recipeId: string; recipeImplementation: RecipeInterface; req: BaseRequest; res: BaseResponse }

    Type declaration

    RecipeInterface: { getDashboardBundleLocation: any; shouldAllowAccess: any }

    Type declaration

    • getDashboardBundleLocation:function
      • getDashboardBundleLocation(input: { userContext: UserContext }): Promise<string>
    • shouldAllowAccess:function
      • shouldAllowAccess(input: { config: TypeNormalisedInput; req: BaseRequest; userContext: UserContext }): Promise<boolean>

    Functions

    • init(config?: TypeInput): RecipeListFunction

    Legend

    • Variable
    • Function
    • Function with type parameter
    • Type alias
    • Type alias with type parameter
    • Class
    • Class with type parameter
    • Interface

    Settings

    Theme

    Generated using TypeDoc

    \ No newline at end of file diff --git a/docs/modules/recipe_emailpassword.html b/docs/modules/recipe_emailpassword.html index 01e9a9c1a..4b2dde830 100644 --- a/docs/modules/recipe_emailpassword.html +++ b/docs/modules/recipe_emailpassword.html @@ -1,5 +1,5 @@ -recipe/emailpassword | supertokens-node
    Options
    All
    • Public
    • Public/Protected
    • All
    Menu

    Module recipe/emailpassword

    Index

    Type Aliases

    APIInterface: { emailExistsGET: undefined | ((input: { email: string; options: APIOptions; tenantId: string; userContext: UserContext }) => Promise<{ exists: boolean; status: "OK" } | GeneralErrorResponse>); generatePasswordResetTokenPOST: undefined | ((input: { formFields: { id: string; value: string }[]; options: APIOptions; tenantId: string; userContext: UserContext }) => Promise<{ status: "OK" } | { reason: string; status: "PASSWORD_RESET_NOT_ALLOWED" } | GeneralErrorResponse>); passwordResetPOST: undefined | ((input: { formFields: { id: string; value: string }[]; options: APIOptions; tenantId: string; token: string; userContext: UserContext }) => Promise<{ email: string; status: "OK"; user: User } | { status: "RESET_PASSWORD_INVALID_TOKEN_ERROR" } | { failureReason: string; status: "PASSWORD_POLICY_VIOLATED_ERROR" } | GeneralErrorResponse>); signInPOST: undefined | ((input: { formFields: { id: string; value: string }[]; options: APIOptions; session: SessionContainer | undefined; tenantId: string; userContext: UserContext }) => Promise<{ session: SessionContainer; status: "OK"; user: User } | { reason: string; status: "SIGN_IN_NOT_ALLOWED" } | { status: "WRONG_CREDENTIALS_ERROR" } | GeneralErrorResponse>); signUpPOST: undefined | ((input: { formFields: { id: string; value: string }[]; options: APIOptions; session: SessionContainer | undefined; tenantId: string; userContext: UserContext }) => Promise<{ session: SessionContainer; status: "OK"; user: User } | { reason: string; status: "SIGN_UP_NOT_ALLOWED" } | { status: "EMAIL_ALREADY_EXISTS_ERROR" } | GeneralErrorResponse>) }

    Type declaration

    • emailExistsGET: undefined | ((input: { email: string; options: APIOptions; tenantId: string; userContext: UserContext }) => Promise<{ exists: boolean; status: "OK" } | GeneralErrorResponse>)
    • generatePasswordResetTokenPOST: undefined | ((input: { formFields: { id: string; value: string }[]; options: APIOptions; tenantId: string; userContext: UserContext }) => Promise<{ status: "OK" } | { reason: string; status: "PASSWORD_RESET_NOT_ALLOWED" } | GeneralErrorResponse>)
    • passwordResetPOST: undefined | ((input: { formFields: { id: string; value: string }[]; options: APIOptions; tenantId: string; token: string; userContext: UserContext }) => Promise<{ email: string; status: "OK"; user: User } | { status: "RESET_PASSWORD_INVALID_TOKEN_ERROR" } | { failureReason: string; status: "PASSWORD_POLICY_VIOLATED_ERROR" } | GeneralErrorResponse>)
    • signInPOST: undefined | ((input: { formFields: { id: string; value: string }[]; options: APIOptions; session: SessionContainer | undefined; tenantId: string; userContext: UserContext }) => Promise<{ session: SessionContainer; status: "OK"; user: User } | { reason: string; status: "SIGN_IN_NOT_ALLOWED" } | { status: "WRONG_CREDENTIALS_ERROR" } | GeneralErrorResponse>)
    • signUpPOST: undefined | ((input: { formFields: { id: string; value: string }[]; options: APIOptions; session: SessionContainer | undefined; tenantId: string; userContext: UserContext }) => Promise<{ session: SessionContainer; status: "OK"; user: User } | { reason: string; status: "SIGN_UP_NOT_ALLOWED" } | { status: "EMAIL_ALREADY_EXISTS_ERROR" } | GeneralErrorResponse>)
    APIOptions: { appInfo: NormalisedAppinfo; config: TypeNormalisedInput; emailDelivery: default<TypeEmailPasswordEmailDeliveryInput>; isInServerlessEnv: boolean; recipeId: string; recipeImplementation: RecipeInterface; req: BaseRequest; res: BaseResponse }

    Type declaration

    RecipeInterface: { consumePasswordResetToken: any; createNewRecipeUser: any; createResetPasswordToken: any; signIn: any; signUp: any; updateEmailOrPassword: any; verifyCredentials: any }

    Type declaration

    • consumePasswordResetToken:function
      • consumePasswordResetToken(input: { tenantId: string; token: string; userContext: UserContext }): Promise<{ email: string; status: "OK"; userId: string } | { status: "RESET_PASSWORD_INVALID_TOKEN_ERROR" }>
      • Parameters

        • input: { tenantId: string; token: string; userContext: UserContext }
          • tenantId: string
          • token: string
          • userContext: UserContext

        Returns Promise<{ email: string; status: "OK"; userId: string } | { status: "RESET_PASSWORD_INVALID_TOKEN_ERROR" }>

    • createNewRecipeUser:function
      • createNewRecipeUser(input: { email: string; password: string; tenantId: string; userContext: UserContext }): Promise<{ recipeUserId: RecipeUserId; status: "OK"; user: User } | { status: "EMAIL_ALREADY_EXISTS_ERROR" }>
      • Parameters

        • input: { email: string; password: string; tenantId: string; userContext: UserContext }
          • email: string
          • password: string
          • tenantId: string
          • userContext: UserContext

        Returns Promise<{ recipeUserId: RecipeUserId; status: "OK"; user: User } | { status: "EMAIL_ALREADY_EXISTS_ERROR" }>

    • createResetPasswordToken:function
      • createResetPasswordToken(input: { email: string; tenantId: string; userContext: UserContext; userId: string }): Promise<{ status: "OK"; token: string } | { status: "UNKNOWN_USER_ID_ERROR" }>
      • +recipe/emailpassword | supertokens-node
        Options
        All
        • Public
        • Public/Protected
        • All
        Menu

        Module recipe/emailpassword

        Index

        Type Aliases

        APIInterface: { emailExistsGET: undefined | ((input: { email: string; options: APIOptions; tenantId: string; userContext: UserContext }) => Promise<{ exists: boolean; status: "OK" } | GeneralErrorResponse>); generatePasswordResetTokenPOST: undefined | ((input: { formFields: { id: string; value: string }[]; options: APIOptions; tenantId: string; userContext: UserContext }) => Promise<{ status: "OK" } | { reason: string; status: "PASSWORD_RESET_NOT_ALLOWED" } | GeneralErrorResponse>); passwordResetPOST: undefined | ((input: { formFields: { id: string; value: string }[]; options: APIOptions; tenantId: string; token: string; userContext: UserContext }) => Promise<{ email: string; status: "OK"; user: User } | { status: "RESET_PASSWORD_INVALID_TOKEN_ERROR" } | { failureReason: string; status: "PASSWORD_POLICY_VIOLATED_ERROR" } | GeneralErrorResponse>); signInPOST: undefined | ((input: { formFields: { id: string; value: string }[]; options: APIOptions; session: SessionContainer | undefined; tenantId: string; userContext: UserContext }) => Promise<{ session: SessionContainer; status: "OK"; user: User } | { reason: string; status: "SIGN_IN_NOT_ALLOWED" } | { status: "WRONG_CREDENTIALS_ERROR" } | GeneralErrorResponse>); signUpPOST: undefined | ((input: { formFields: { id: string; value: string }[]; options: APIOptions; session: SessionContainer | undefined; tenantId: string; userContext: UserContext }) => Promise<{ session: SessionContainer; status: "OK"; user: User } | { reason: string; status: "SIGN_UP_NOT_ALLOWED" } | { status: "EMAIL_ALREADY_EXISTS_ERROR" } | GeneralErrorResponse>) }

        Type declaration

        • emailExistsGET: undefined | ((input: { email: string; options: APIOptions; tenantId: string; userContext: UserContext }) => Promise<{ exists: boolean; status: "OK" } | GeneralErrorResponse>)
        • generatePasswordResetTokenPOST: undefined | ((input: { formFields: { id: string; value: string }[]; options: APIOptions; tenantId: string; userContext: UserContext }) => Promise<{ status: "OK" } | { reason: string; status: "PASSWORD_RESET_NOT_ALLOWED" } | GeneralErrorResponse>)
        • passwordResetPOST: undefined | ((input: { formFields: { id: string; value: string }[]; options: APIOptions; tenantId: string; token: string; userContext: UserContext }) => Promise<{ email: string; status: "OK"; user: User } | { status: "RESET_PASSWORD_INVALID_TOKEN_ERROR" } | { failureReason: string; status: "PASSWORD_POLICY_VIOLATED_ERROR" } | GeneralErrorResponse>)
        • signInPOST: undefined | ((input: { formFields: { id: string; value: string }[]; options: APIOptions; session: SessionContainer | undefined; tenantId: string; userContext: UserContext }) => Promise<{ session: SessionContainer; status: "OK"; user: User } | { reason: string; status: "SIGN_IN_NOT_ALLOWED" } | { status: "WRONG_CREDENTIALS_ERROR" } | GeneralErrorResponse>)
        • signUpPOST: undefined | ((input: { formFields: { id: string; value: string }[]; options: APIOptions; session: SessionContainer | undefined; tenantId: string; userContext: UserContext }) => Promise<{ session: SessionContainer; status: "OK"; user: User } | { reason: string; status: "SIGN_UP_NOT_ALLOWED" } | { status: "EMAIL_ALREADY_EXISTS_ERROR" } | GeneralErrorResponse>)
        APIOptions: { appInfo: NormalisedAppinfo; config: TypeNormalisedInput; emailDelivery: default<TypeEmailPasswordEmailDeliveryInput>; isInServerlessEnv: boolean; recipeId: string; recipeImplementation: RecipeInterface; req: BaseRequest; res: BaseResponse }

        Type declaration

        RecipeInterface: { consumePasswordResetToken: any; createNewRecipeUser: any; createResetPasswordToken: any; signIn: any; signUp: any; updateEmailOrPassword: any; verifyCredentials: any }

        Type declaration

        • consumePasswordResetToken:function
          • consumePasswordResetToken(input: { tenantId: string; token: string; userContext: UserContext }): Promise<{ email: string; status: "OK"; userId: string } | { status: "RESET_PASSWORD_INVALID_TOKEN_ERROR" }>
          • Parameters

            • input: { tenantId: string; token: string; userContext: UserContext }
              • tenantId: string
              • token: string
              • userContext: UserContext

            Returns Promise<{ email: string; status: "OK"; userId: string } | { status: "RESET_PASSWORD_INVALID_TOKEN_ERROR" }>

        • createNewRecipeUser:function
          • createNewRecipeUser(input: { email: string; password: string; tenantId: string; userContext: UserContext }): Promise<{ recipeUserId: RecipeUserId; status: "OK"; user: User } | { status: "EMAIL_ALREADY_EXISTS_ERROR" }>
          • Parameters

            • input: { email: string; password: string; tenantId: string; userContext: UserContext }
              • email: string
              • password: string
              • tenantId: string
              • userContext: UserContext

            Returns Promise<{ recipeUserId: RecipeUserId; status: "OK"; user: User } | { status: "EMAIL_ALREADY_EXISTS_ERROR" }>

        • createResetPasswordToken:function
          • createResetPasswordToken(input: { email: string; tenantId: string; userContext: UserContext; userId: string }): Promise<{ status: "OK"; token: string } | { status: "UNKNOWN_USER_ID_ERROR" }>
          • We pass in the email as well to this function cause the input userId may not be associated with an emailpassword account. In this case, we need to know which email to use to create an emailpassword account later on.

            -

            Parameters

            • input: { email: string; tenantId: string; userContext: UserContext; userId: string }
              • email: string
              • tenantId: string
              • userContext: UserContext
              • userId: string

            Returns Promise<{ status: "OK"; token: string } | { status: "UNKNOWN_USER_ID_ERROR" }>

        • signIn:function
          • signIn(input: { email: string; password: string; session: SessionContainer | undefined; tenantId: string; userContext: UserContext }): Promise<{ recipeUserId: RecipeUserId; status: "OK"; user: User } | { status: "WRONG_CREDENTIALS_ERROR" } | { reason: "EMAIL_VERIFICATION_REQUIRED" | "RECIPE_USER_ID_ALREADY_LINKED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR" | "ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR" | "SESSION_USER_ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR"; status: "LINKING_TO_SESSION_USER_FAILED" }>
          • Parameters

            • input: { email: string; password: string; session: SessionContainer | undefined; tenantId: string; userContext: UserContext }
              • email: string
              • password: string
              • session: SessionContainer | undefined
              • tenantId: string
              • userContext: UserContext

            Returns Promise<{ recipeUserId: RecipeUserId; status: "OK"; user: User } | { status: "WRONG_CREDENTIALS_ERROR" } | { reason: "EMAIL_VERIFICATION_REQUIRED" | "RECIPE_USER_ID_ALREADY_LINKED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR" | "ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR" | "SESSION_USER_ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR"; status: "LINKING_TO_SESSION_USER_FAILED" }>

        • signUp:function
          • signUp(input: { email: string; password: string; session: SessionContainer | undefined; tenantId: string; userContext: UserContext }): Promise<{ recipeUserId: RecipeUserId; status: "OK"; user: User } | { status: "EMAIL_ALREADY_EXISTS_ERROR" } | { reason: "EMAIL_VERIFICATION_REQUIRED" | "RECIPE_USER_ID_ALREADY_LINKED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR" | "ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR" | "SESSION_USER_ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR"; status: "LINKING_TO_SESSION_USER_FAILED" }>
          • Parameters

            • input: { email: string; password: string; session: SessionContainer | undefined; tenantId: string; userContext: UserContext }
              • email: string
              • password: string
              • session: SessionContainer | undefined
              • tenantId: string
              • userContext: UserContext

            Returns Promise<{ recipeUserId: RecipeUserId; status: "OK"; user: User } | { status: "EMAIL_ALREADY_EXISTS_ERROR" } | { reason: "EMAIL_VERIFICATION_REQUIRED" | "RECIPE_USER_ID_ALREADY_LINKED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR" | "ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR" | "SESSION_USER_ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR"; status: "LINKING_TO_SESSION_USER_FAILED" }>

        • updateEmailOrPassword:function
          • updateEmailOrPassword(input: { applyPasswordPolicy?: boolean; email?: string; password?: string; recipeUserId: RecipeUserId; tenantIdForPasswordPolicy: string; userContext: UserContext }): Promise<{ status: "OK" | "UNKNOWN_USER_ID_ERROR" | "EMAIL_ALREADY_EXISTS_ERROR" } | { reason: string; status: "EMAIL_CHANGE_NOT_ALLOWED_ERROR" } | { failureReason: string; status: "PASSWORD_POLICY_VIOLATED_ERROR" }>
          • Parameters

            • input: { applyPasswordPolicy?: boolean; email?: string; password?: string; recipeUserId: RecipeUserId; tenantIdForPasswordPolicy: string; userContext: UserContext }
              • Optional applyPasswordPolicy?: boolean
              • Optional email?: string
              • Optional password?: string
              • recipeUserId: RecipeUserId
              • tenantIdForPasswordPolicy: string
              • userContext: UserContext

            Returns Promise<{ status: "OK" | "UNKNOWN_USER_ID_ERROR" | "EMAIL_ALREADY_EXISTS_ERROR" } | { reason: string; status: "EMAIL_CHANGE_NOT_ALLOWED_ERROR" } | { failureReason: string; status: "PASSWORD_POLICY_VIOLATED_ERROR" }>

        • verifyCredentials:function
          • verifyCredentials(input: { email: string; password: string; tenantId: string; userContext: UserContext }): Promise<{ recipeUserId: RecipeUserId; status: "OK"; user: User } | { status: "WRONG_CREDENTIALS_ERROR" }>
          • Parameters

            • input: { email: string; password: string; tenantId: string; userContext: UserContext }
              • email: string
              • password: string
              • tenantId: string
              • userContext: UserContext

            Returns Promise<{ recipeUserId: RecipeUserId; status: "OK"; user: User } | { status: "WRONG_CREDENTIALS_ERROR" }>

        Variables

        Error: typeof default = Wrapper.Error

        Functions

        • consumePasswordResetToken(tenantId: string, token: string, userContext?: Record<string, any>): Promise<{ email: string; status: "OK"; userId: string } | { status: "RESET_PASSWORD_INVALID_TOKEN_ERROR" }>
        • Parameters

          • tenantId: string
          • token: string
          • Optional userContext: Record<string, any>

          Returns Promise<{ email: string; status: "OK"; userId: string } | { status: "RESET_PASSWORD_INVALID_TOKEN_ERROR" }>

        • createResetPasswordLink(tenantId: string, userId: string, email: string, userContext?: Record<string, any>): Promise<{ link: string; status: "OK" } | { status: "UNKNOWN_USER_ID_ERROR" }>
        • Parameters

          • tenantId: string
          • userId: string
          • email: string
          • Optional userContext: Record<string, any>

          Returns Promise<{ link: string; status: "OK" } | { status: "UNKNOWN_USER_ID_ERROR" }>

        • createResetPasswordToken(tenantId: string, userId: string, email: string, userContext?: Record<string, any>): Promise<{ status: "OK"; token: string } | { status: "UNKNOWN_USER_ID_ERROR" }>
        • Parameters

          • tenantId: string
          • userId: string
          • email: string
          • Optional userContext: Record<string, any>

          Returns Promise<{ status: "OK"; token: string } | { status: "UNKNOWN_USER_ID_ERROR" }>

        • init(config?: TypeInput): RecipeListFunction
        • resetPasswordUsingToken(tenantId: string, token: string, newPassword: string, userContext?: Record<string, any>): Promise<{ status: "OK" | "UNKNOWN_USER_ID_ERROR" | "RESET_PASSWORD_INVALID_TOKEN_ERROR" } | { failureReason: string; status: "PASSWORD_POLICY_VIOLATED_ERROR" }>
        • Parameters

          • tenantId: string
          • token: string
          • newPassword: string
          • Optional userContext: Record<string, any>

          Returns Promise<{ status: "OK" | "UNKNOWN_USER_ID_ERROR" | "RESET_PASSWORD_INVALID_TOKEN_ERROR" } | { failureReason: string; status: "PASSWORD_POLICY_VIOLATED_ERROR" }>

        • sendEmail(input: TypeEmailPasswordPasswordResetEmailDeliveryInput & { userContext?: Record<string, any> }): Promise<void>
        • sendResetPasswordEmail(tenantId: string, userId: string, email: string, userContext?: Record<string, any>): Promise<{ status: "OK" | "UNKNOWN_USER_ID_ERROR" }>
        • Parameters

          • tenantId: string
          • userId: string
          • email: string
          • Optional userContext: Record<string, any>

          Returns Promise<{ status: "OK" | "UNKNOWN_USER_ID_ERROR" }>

        • signIn(tenantId: string, email: string, password: string, session?: undefined, userContext?: Record<string, any>): Promise<{ recipeUserId: RecipeUserId; status: "OK"; user: User } | { status: "WRONG_CREDENTIALS_ERROR" }>
        • signIn(tenantId: string, email: string, password: string, session: SessionContainer, userContext?: Record<string, any>): Promise<{ recipeUserId: RecipeUserId; status: "OK"; user: User } | { status: "WRONG_CREDENTIALS_ERROR" } | { reason: "ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR" | "RECIPE_USER_ID_ALREADY_LINKED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR" | "EMAIL_VERIFICATION_REQUIRED" | "SESSION_USER_ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR"; status: "LINKING_TO_SESSION_USER_FAILED" }>
        • Parameters

          • tenantId: string
          • email: string
          • password: string
          • Optional session: undefined
          • Optional userContext: Record<string, any>

          Returns Promise<{ recipeUserId: RecipeUserId; status: "OK"; user: User } | { status: "WRONG_CREDENTIALS_ERROR" }>

        • Parameters

          • tenantId: string
          • email: string
          • password: string
          • session: SessionContainer
          • Optional userContext: Record<string, any>

          Returns Promise<{ recipeUserId: RecipeUserId; status: "OK"; user: User } | { status: "WRONG_CREDENTIALS_ERROR" } | { reason: "ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR" | "RECIPE_USER_ID_ALREADY_LINKED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR" | "EMAIL_VERIFICATION_REQUIRED" | "SESSION_USER_ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR"; status: "LINKING_TO_SESSION_USER_FAILED" }>

        • signUp(tenantId: string, email: string, password: string, session?: undefined, userContext?: Record<string, any>): Promise<{ recipeUserId: RecipeUserId; status: "OK"; user: User } | { status: "EMAIL_ALREADY_EXISTS_ERROR" }>
        • signUp(tenantId: string, email: string, password: string, session: SessionContainer, userContext?: Record<string, any>): Promise<{ recipeUserId: RecipeUserId; status: "OK"; user: User } | { status: "EMAIL_ALREADY_EXISTS_ERROR" } | { reason: "ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR" | "RECIPE_USER_ID_ALREADY_LINKED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR" | "EMAIL_VERIFICATION_REQUIRED" | "SESSION_USER_ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR"; status: "LINKING_TO_SESSION_USER_FAILED" }>
        • Parameters

          • tenantId: string
          • email: string
          • password: string
          • Optional session: undefined
          • Optional userContext: Record<string, any>

          Returns Promise<{ recipeUserId: RecipeUserId; status: "OK"; user: User } | { status: "EMAIL_ALREADY_EXISTS_ERROR" }>

        • Parameters

          • tenantId: string
          • email: string
          • password: string
          • session: SessionContainer
          • Optional userContext: Record<string, any>

          Returns Promise<{ recipeUserId: RecipeUserId; status: "OK"; user: User } | { status: "EMAIL_ALREADY_EXISTS_ERROR" } | { reason: "ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR" | "RECIPE_USER_ID_ALREADY_LINKED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR" | "EMAIL_VERIFICATION_REQUIRED" | "SESSION_USER_ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR"; status: "LINKING_TO_SESSION_USER_FAILED" }>

        • updateEmailOrPassword(input: { applyPasswordPolicy?: boolean; email?: string; password?: string; recipeUserId: RecipeUserId; tenantIdForPasswordPolicy?: string; userContext?: Record<string, any> }): Promise<{ status: "OK" | "EMAIL_ALREADY_EXISTS_ERROR" | "UNKNOWN_USER_ID_ERROR" } | { reason: string; status: "EMAIL_CHANGE_NOT_ALLOWED_ERROR" } | { failureReason: string; status: "PASSWORD_POLICY_VIOLATED_ERROR" }>
        • Parameters

          • input: { applyPasswordPolicy?: boolean; email?: string; password?: string; recipeUserId: RecipeUserId; tenantIdForPasswordPolicy?: string; userContext?: Record<string, any> }
            • Optional applyPasswordPolicy?: boolean
            • Optional email?: string
            • Optional password?: string
            • recipeUserId: RecipeUserId
            • Optional tenantIdForPasswordPolicy?: string
            • Optional userContext?: Record<string, any>

          Returns Promise<{ status: "OK" | "EMAIL_ALREADY_EXISTS_ERROR" | "UNKNOWN_USER_ID_ERROR" } | { reason: string; status: "EMAIL_CHANGE_NOT_ALLOWED_ERROR" } | { failureReason: string; status: "PASSWORD_POLICY_VIOLATED_ERROR" }>

        • verifyCredentials(tenantId: string, email: string, password: string, userContext?: Record<string, any>): Promise<{ status: "OK" | "WRONG_CREDENTIALS_ERROR" }>
        • Parameters

          • tenantId: string
          • email: string
          • password: string
          • Optional userContext: Record<string, any>

          Returns Promise<{ status: "OK" | "WRONG_CREDENTIALS_ERROR" }>

        Legend

        • Variable
        • Function
        • Function with type parameter
        • Type alias
        • Type alias with type parameter
        • Class
        • Class with type parameter
        • Interface

        Settings

        Theme

        Generated using TypeDoc

        \ No newline at end of file +

        Parameters

        • input: { email: string; tenantId: string; userContext: UserContext; userId: string }
          • email: string
          • tenantId: string
          • userContext: UserContext
          • userId: string

        Returns Promise<{ status: "OK"; token: string } | { status: "UNKNOWN_USER_ID_ERROR" }>

    • signIn:function
      • signIn(input: { email: string; password: string; session: SessionContainer | undefined; tenantId: string; userContext: UserContext }): Promise<{ recipeUserId: RecipeUserId; status: "OK"; user: User } | { status: "WRONG_CREDENTIALS_ERROR" } | { reason: "EMAIL_VERIFICATION_REQUIRED" | "RECIPE_USER_ID_ALREADY_LINKED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR" | "ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR" | "SESSION_USER_ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR"; status: "LINKING_TO_SESSION_USER_FAILED" }>
      • Parameters

        • input: { email: string; password: string; session: SessionContainer | undefined; tenantId: string; userContext: UserContext }
          • email: string
          • password: string
          • session: SessionContainer | undefined
          • tenantId: string
          • userContext: UserContext

        Returns Promise<{ recipeUserId: RecipeUserId; status: "OK"; user: User } | { status: "WRONG_CREDENTIALS_ERROR" } | { reason: "EMAIL_VERIFICATION_REQUIRED" | "RECIPE_USER_ID_ALREADY_LINKED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR" | "ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR" | "SESSION_USER_ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR"; status: "LINKING_TO_SESSION_USER_FAILED" }>

    • signUp:function
      • signUp(input: { email: string; password: string; session: SessionContainer | undefined; tenantId: string; userContext: UserContext }): Promise<{ recipeUserId: RecipeUserId; status: "OK"; user: User } | { status: "EMAIL_ALREADY_EXISTS_ERROR" } | { reason: "EMAIL_VERIFICATION_REQUIRED" | "RECIPE_USER_ID_ALREADY_LINKED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR" | "ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR" | "SESSION_USER_ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR"; status: "LINKING_TO_SESSION_USER_FAILED" }>
      • Parameters

        • input: { email: string; password: string; session: SessionContainer | undefined; tenantId: string; userContext: UserContext }
          • email: string
          • password: string
          • session: SessionContainer | undefined
          • tenantId: string
          • userContext: UserContext

        Returns Promise<{ recipeUserId: RecipeUserId; status: "OK"; user: User } | { status: "EMAIL_ALREADY_EXISTS_ERROR" } | { reason: "EMAIL_VERIFICATION_REQUIRED" | "RECIPE_USER_ID_ALREADY_LINKED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR" | "ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR" | "SESSION_USER_ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR"; status: "LINKING_TO_SESSION_USER_FAILED" }>

    • updateEmailOrPassword:function
      • updateEmailOrPassword(input: { applyPasswordPolicy?: boolean; email?: string; password?: string; recipeUserId: RecipeUserId; tenantIdForPasswordPolicy: string; userContext: UserContext }): Promise<{ status: "OK" | "UNKNOWN_USER_ID_ERROR" | "EMAIL_ALREADY_EXISTS_ERROR" } | { reason: string; status: "EMAIL_CHANGE_NOT_ALLOWED_ERROR" } | { failureReason: string; status: "PASSWORD_POLICY_VIOLATED_ERROR" }>
      • Parameters

        • input: { applyPasswordPolicy?: boolean; email?: string; password?: string; recipeUserId: RecipeUserId; tenantIdForPasswordPolicy: string; userContext: UserContext }
          • Optional applyPasswordPolicy?: boolean
          • Optional email?: string
          • Optional password?: string
          • recipeUserId: RecipeUserId
          • tenantIdForPasswordPolicy: string
          • userContext: UserContext

        Returns Promise<{ status: "OK" | "UNKNOWN_USER_ID_ERROR" | "EMAIL_ALREADY_EXISTS_ERROR" } | { reason: string; status: "EMAIL_CHANGE_NOT_ALLOWED_ERROR" } | { failureReason: string; status: "PASSWORD_POLICY_VIOLATED_ERROR" }>

    • verifyCredentials:function
      • verifyCredentials(input: { email: string; password: string; tenantId: string; userContext: UserContext }): Promise<{ recipeUserId: RecipeUserId; status: "OK"; user: User } | { status: "WRONG_CREDENTIALS_ERROR" }>
      • Parameters

        • input: { email: string; password: string; tenantId: string; userContext: UserContext }
          • email: string
          • password: string
          • tenantId: string
          • userContext: UserContext

        Returns Promise<{ recipeUserId: RecipeUserId; status: "OK"; user: User } | { status: "WRONG_CREDENTIALS_ERROR" }>

    Variables

    Error: typeof default = Wrapper.Error

    Functions

    • consumePasswordResetToken(tenantId: string, token: string, userContext?: Record<string, any>): Promise<{ email: string; status: "OK"; userId: string } | { status: "RESET_PASSWORD_INVALID_TOKEN_ERROR" }>
    • Parameters

      • tenantId: string
      • token: string
      • Optional userContext: Record<string, any>

      Returns Promise<{ email: string; status: "OK"; userId: string } | { status: "RESET_PASSWORD_INVALID_TOKEN_ERROR" }>

    • createResetPasswordLink(tenantId: string, userId: string, email: string, userContext?: Record<string, any>): Promise<{ link: string; status: "OK" } | { status: "UNKNOWN_USER_ID_ERROR" }>
    • Parameters

      • tenantId: string
      • userId: string
      • email: string
      • Optional userContext: Record<string, any>

      Returns Promise<{ link: string; status: "OK" } | { status: "UNKNOWN_USER_ID_ERROR" }>

    • createResetPasswordToken(tenantId: string, userId: string, email: string, userContext?: Record<string, any>): Promise<{ status: "OK"; token: string } | { status: "UNKNOWN_USER_ID_ERROR" }>
    • Parameters

      • tenantId: string
      • userId: string
      • email: string
      • Optional userContext: Record<string, any>

      Returns Promise<{ status: "OK"; token: string } | { status: "UNKNOWN_USER_ID_ERROR" }>

    • init(config?: TypeInput): RecipeListFunction
    • resetPasswordUsingToken(tenantId: string, token: string, newPassword: string, userContext?: Record<string, any>): Promise<{ status: "OK" | "UNKNOWN_USER_ID_ERROR" | "RESET_PASSWORD_INVALID_TOKEN_ERROR" } | { failureReason: string; status: "PASSWORD_POLICY_VIOLATED_ERROR" }>
    • Parameters

      • tenantId: string
      • token: string
      • newPassword: string
      • Optional userContext: Record<string, any>

      Returns Promise<{ status: "OK" | "UNKNOWN_USER_ID_ERROR" | "RESET_PASSWORD_INVALID_TOKEN_ERROR" } | { failureReason: string; status: "PASSWORD_POLICY_VIOLATED_ERROR" }>

    • sendEmail(input: TypeEmailPasswordPasswordResetEmailDeliveryInput & { userContext?: Record<string, any> }): Promise<void>
    • sendResetPasswordEmail(tenantId: string, userId: string, email: string, userContext?: Record<string, any>): Promise<{ status: "OK" | "UNKNOWN_USER_ID_ERROR" }>
    • Parameters

      • tenantId: string
      • userId: string
      • email: string
      • Optional userContext: Record<string, any>

      Returns Promise<{ status: "OK" | "UNKNOWN_USER_ID_ERROR" }>

    • signIn(tenantId: string, email: string, password: string, session?: undefined, userContext?: Record<string, any>): Promise<{ recipeUserId: RecipeUserId; status: "OK"; user: User } | { status: "WRONG_CREDENTIALS_ERROR" }>
    • signIn(tenantId: string, email: string, password: string, session: SessionContainer, userContext?: Record<string, any>): Promise<{ recipeUserId: RecipeUserId; status: "OK"; user: User } | { status: "WRONG_CREDENTIALS_ERROR" } | { reason: "ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR" | "RECIPE_USER_ID_ALREADY_LINKED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR" | "EMAIL_VERIFICATION_REQUIRED" | "SESSION_USER_ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR"; status: "LINKING_TO_SESSION_USER_FAILED" }>
    • Parameters

      • tenantId: string
      • email: string
      • password: string
      • Optional session: undefined
      • Optional userContext: Record<string, any>

      Returns Promise<{ recipeUserId: RecipeUserId; status: "OK"; user: User } | { status: "WRONG_CREDENTIALS_ERROR" }>

    • Parameters

      • tenantId: string
      • email: string
      • password: string
      • session: SessionContainer
      • Optional userContext: Record<string, any>

      Returns Promise<{ recipeUserId: RecipeUserId; status: "OK"; user: User } | { status: "WRONG_CREDENTIALS_ERROR" } | { reason: "ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR" | "RECIPE_USER_ID_ALREADY_LINKED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR" | "EMAIL_VERIFICATION_REQUIRED" | "SESSION_USER_ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR"; status: "LINKING_TO_SESSION_USER_FAILED" }>

    • signUp(tenantId: string, email: string, password: string, session?: undefined, userContext?: Record<string, any>): Promise<{ recipeUserId: RecipeUserId; status: "OK"; user: User } | { status: "EMAIL_ALREADY_EXISTS_ERROR" }>
    • signUp(tenantId: string, email: string, password: string, session: SessionContainer, userContext?: Record<string, any>): Promise<{ recipeUserId: RecipeUserId; status: "OK"; user: User } | { status: "EMAIL_ALREADY_EXISTS_ERROR" } | { reason: "ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR" | "RECIPE_USER_ID_ALREADY_LINKED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR" | "EMAIL_VERIFICATION_REQUIRED" | "SESSION_USER_ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR"; status: "LINKING_TO_SESSION_USER_FAILED" }>
    • Parameters

      • tenantId: string
      • email: string
      • password: string
      • Optional session: undefined
      • Optional userContext: Record<string, any>

      Returns Promise<{ recipeUserId: RecipeUserId; status: "OK"; user: User } | { status: "EMAIL_ALREADY_EXISTS_ERROR" }>

    • Parameters

      • tenantId: string
      • email: string
      • password: string
      • session: SessionContainer
      • Optional userContext: Record<string, any>

      Returns Promise<{ recipeUserId: RecipeUserId; status: "OK"; user: User } | { status: "EMAIL_ALREADY_EXISTS_ERROR" } | { reason: "ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR" | "RECIPE_USER_ID_ALREADY_LINKED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR" | "EMAIL_VERIFICATION_REQUIRED" | "SESSION_USER_ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR"; status: "LINKING_TO_SESSION_USER_FAILED" }>

    • updateEmailOrPassword(input: { applyPasswordPolicy?: boolean; email?: string; password?: string; recipeUserId: RecipeUserId; tenantIdForPasswordPolicy?: string; userContext?: Record<string, any> }): Promise<{ status: "OK" | "EMAIL_ALREADY_EXISTS_ERROR" | "UNKNOWN_USER_ID_ERROR" } | { reason: string; status: "EMAIL_CHANGE_NOT_ALLOWED_ERROR" } | { failureReason: string; status: "PASSWORD_POLICY_VIOLATED_ERROR" }>
    • Parameters

      • input: { applyPasswordPolicy?: boolean; email?: string; password?: string; recipeUserId: RecipeUserId; tenantIdForPasswordPolicy?: string; userContext?: Record<string, any> }
        • Optional applyPasswordPolicy?: boolean
        • Optional email?: string
        • Optional password?: string
        • recipeUserId: RecipeUserId
        • Optional tenantIdForPasswordPolicy?: string
        • Optional userContext?: Record<string, any>

      Returns Promise<{ status: "OK" | "EMAIL_ALREADY_EXISTS_ERROR" | "UNKNOWN_USER_ID_ERROR" } | { reason: string; status: "EMAIL_CHANGE_NOT_ALLOWED_ERROR" } | { failureReason: string; status: "PASSWORD_POLICY_VIOLATED_ERROR" }>

    • verifyCredentials(tenantId: string, email: string, password: string, userContext?: Record<string, any>): Promise<{ status: "OK" | "WRONG_CREDENTIALS_ERROR" }>
    • Parameters

      • tenantId: string
      • email: string
      • password: string
      • Optional userContext: Record<string, any>

      Returns Promise<{ status: "OK" | "WRONG_CREDENTIALS_ERROR" }>

    Legend

    • Variable
    • Function
    • Function with type parameter
    • Type alias
    • Type alias with type parameter
    • Class
    • Class with type parameter
    • Interface

    Settings

    Theme

    Generated using TypeDoc

    \ No newline at end of file diff --git a/docs/modules/recipe_emailverification.html b/docs/modules/recipe_emailverification.html index 1aa3a9922..14add5217 100644 --- a/docs/modules/recipe_emailverification.html +++ b/docs/modules/recipe_emailverification.html @@ -1 +1 @@ -recipe/emailverification | supertokens-node
    Options
    All
    • Public
    • Public/Protected
    • All
    Menu

    Module recipe/emailverification

    Index

    Type Aliases

    APIInterface: { generateEmailVerifyTokenPOST: undefined | ((input: { options: APIOptions; session: SessionContainer; userContext: UserContext }) => Promise<{ status: "OK" } | { newSession?: SessionContainer; status: "EMAIL_ALREADY_VERIFIED_ERROR" } | GeneralErrorResponse>); isEmailVerifiedGET: undefined | ((input: { options: APIOptions; session: SessionContainer; userContext: UserContext }) => Promise<{ isVerified: boolean; newSession?: SessionContainer; status: "OK" } | GeneralErrorResponse>); verifyEmailPOST: undefined | ((input: { options: APIOptions; session: SessionContainer | undefined; tenantId: string; token: string; userContext: UserContext }) => Promise<{ newSession?: SessionContainer; status: "OK"; user: UserEmailInfo } | { status: "EMAIL_VERIFICATION_INVALID_TOKEN_ERROR" } | GeneralErrorResponse>) }

    Type declaration

    • generateEmailVerifyTokenPOST: undefined | ((input: { options: APIOptions; session: SessionContainer; userContext: UserContext }) => Promise<{ status: "OK" } | { newSession?: SessionContainer; status: "EMAIL_ALREADY_VERIFIED_ERROR" } | GeneralErrorResponse>)
    • isEmailVerifiedGET: undefined | ((input: { options: APIOptions; session: SessionContainer; userContext: UserContext }) => Promise<{ isVerified: boolean; newSession?: SessionContainer; status: "OK" } | GeneralErrorResponse>)
    • verifyEmailPOST: undefined | ((input: { options: APIOptions; session: SessionContainer | undefined; tenantId: string; token: string; userContext: UserContext }) => Promise<{ newSession?: SessionContainer; status: "OK"; user: UserEmailInfo } | { status: "EMAIL_VERIFICATION_INVALID_TOKEN_ERROR" } | GeneralErrorResponse>)
    APIOptions: { appInfo: NormalisedAppinfo; config: TypeNormalisedInput; emailDelivery: default<TypeEmailVerificationEmailDeliveryInput>; isInServerlessEnv: boolean; recipeId: string; recipeImplementation: RecipeInterface; req: BaseRequest; res: BaseResponse }

    Type declaration

    • appInfo: NormalisedAppinfo
    • config: TypeNormalisedInput
    • emailDelivery: default<TypeEmailVerificationEmailDeliveryInput>
    • isInServerlessEnv: boolean
    • recipeId: string
    • recipeImplementation: RecipeInterface
    • req: BaseRequest
    • res: BaseResponse
    RecipeInterface: { createEmailVerificationToken: any; isEmailVerified: any; revokeEmailVerificationTokens: any; unverifyEmail: any; verifyEmailUsingToken: any }

    Type declaration

    • createEmailVerificationToken:function
      • createEmailVerificationToken(input: { email: string; recipeUserId: RecipeUserId; tenantId: string; userContext: UserContext }): Promise<{ status: "OK"; token: string } | { status: "EMAIL_ALREADY_VERIFIED_ERROR" }>
    • isEmailVerified:function
      • isEmailVerified(input: { email: string; recipeUserId: RecipeUserId; userContext: UserContext }): Promise<boolean>
    • revokeEmailVerificationTokens:function
      • revokeEmailVerificationTokens(input: { email: string; recipeUserId: RecipeUserId; tenantId: string; userContext: UserContext }): Promise<{ status: "OK" }>
    • unverifyEmail:function
      • unverifyEmail(input: { email: string; recipeUserId: RecipeUserId; userContext: UserContext }): Promise<{ status: "OK" }>
    • verifyEmailUsingToken:function
      • verifyEmailUsingToken(input: { attemptAccountLinking: boolean; tenantId: string; token: string; userContext: UserContext }): Promise<{ status: "OK"; user: UserEmailInfo } | { status: "EMAIL_VERIFICATION_INVALID_TOKEN_ERROR" }>
      • Parameters

        • input: { attemptAccountLinking: boolean; tenantId: string; token: string; userContext: UserContext }
          • attemptAccountLinking: boolean
          • tenantId: string
          • token: string
          • userContext: UserContext

        Returns Promise<{ status: "OK"; user: UserEmailInfo } | { status: "EMAIL_VERIFICATION_INVALID_TOKEN_ERROR" }>

    UserEmailInfo: { email: string; recipeUserId: RecipeUserId }

    Type declaration

    Variables

    EmailVerificationClaim: EmailVerificationClaimClass = ...
    Error: typeof default = Wrapper.Error

    Functions

    • createEmailVerificationLink(tenantId: string, recipeUserId: RecipeUserId, email?: string, userContext?: Record<string, any>): Promise<{ link: string; status: "OK" } | { status: "EMAIL_ALREADY_VERIFIED_ERROR" }>
    • createEmailVerificationToken(tenantId: string, recipeUserId: RecipeUserId, email?: string, userContext?: Record<string, any>): Promise<{ status: "OK"; token: string } | { status: "EMAIL_ALREADY_VERIFIED_ERROR" }>
    • init(config: TypeInput): RecipeListFunction
    • isEmailVerified(recipeUserId: RecipeUserId, email?: string, userContext?: Record<string, any>): Promise<boolean>
    • revokeEmailVerificationTokens(tenantId: string, recipeUserId: RecipeUserId, email?: string, userContext?: Record<string, any>): Promise<{ status: string }>
    • sendEmail(input: TypeEmailVerificationEmailDeliveryInput & { userContext?: Record<string, any> }): Promise<void>
    • sendEmailVerificationEmail(tenantId: string, userId: string, recipeUserId: RecipeUserId, email?: string, userContext?: Record<string, any>): Promise<{ status: "OK" } | { status: "EMAIL_ALREADY_VERIFIED_ERROR" }>
    • unverifyEmail(recipeUserId: RecipeUserId, email?: string, userContext?: Record<string, any>): Promise<{ status: string }>
    • verifyEmailUsingToken(tenantId: string, token: string, attemptAccountLinking?: boolean, userContext?: Record<string, any>): Promise<{ status: "OK"; user: UserEmailInfo } | { status: "EMAIL_VERIFICATION_INVALID_TOKEN_ERROR" }>

    Legend

    • Variable
    • Function
    • Function with type parameter
    • Type alias
    • Type alias with type parameter
    • Class
    • Class with type parameter
    • Interface

    Settings

    Theme

    Generated using TypeDoc

    \ No newline at end of file +recipe/emailverification | supertokens-node
    Options
    All
    • Public
    • Public/Protected
    • All
    Menu

    Module recipe/emailverification

    Index

    Type Aliases

    APIInterface: { generateEmailVerifyTokenPOST: undefined | ((input: { options: APIOptions; session: SessionContainer; userContext: UserContext }) => Promise<{ status: "OK" } | { newSession?: SessionContainer; status: "EMAIL_ALREADY_VERIFIED_ERROR" } | GeneralErrorResponse>); isEmailVerifiedGET: undefined | ((input: { options: APIOptions; session: SessionContainer; userContext: UserContext }) => Promise<{ isVerified: boolean; newSession?: SessionContainer; status: "OK" } | GeneralErrorResponse>); verifyEmailPOST: undefined | ((input: { options: APIOptions; session: SessionContainer | undefined; tenantId: string; token: string; userContext: UserContext }) => Promise<{ newSession?: SessionContainer; status: "OK"; user: UserEmailInfo } | { status: "EMAIL_VERIFICATION_INVALID_TOKEN_ERROR" } | GeneralErrorResponse>) }

    Type declaration

    • generateEmailVerifyTokenPOST: undefined | ((input: { options: APIOptions; session: SessionContainer; userContext: UserContext }) => Promise<{ status: "OK" } | { newSession?: SessionContainer; status: "EMAIL_ALREADY_VERIFIED_ERROR" } | GeneralErrorResponse>)
    • isEmailVerifiedGET: undefined | ((input: { options: APIOptions; session: SessionContainer; userContext: UserContext }) => Promise<{ isVerified: boolean; newSession?: SessionContainer; status: "OK" } | GeneralErrorResponse>)
    • verifyEmailPOST: undefined | ((input: { options: APIOptions; session: SessionContainer | undefined; tenantId: string; token: string; userContext: UserContext }) => Promise<{ newSession?: SessionContainer; status: "OK"; user: UserEmailInfo } | { status: "EMAIL_VERIFICATION_INVALID_TOKEN_ERROR" } | GeneralErrorResponse>)
    APIOptions: { appInfo: NormalisedAppinfo; config: TypeNormalisedInput; emailDelivery: default<TypeEmailVerificationEmailDeliveryInput>; isInServerlessEnv: boolean; recipeId: string; recipeImplementation: RecipeInterface; req: BaseRequest; res: BaseResponse }

    Type declaration

    • appInfo: NormalisedAppinfo
    • config: TypeNormalisedInput
    • emailDelivery: default<TypeEmailVerificationEmailDeliveryInput>
    • isInServerlessEnv: boolean
    • recipeId: string
    • recipeImplementation: RecipeInterface
    • req: BaseRequest
    • res: BaseResponse
    RecipeInterface: { createEmailVerificationToken: any; isEmailVerified: any; revokeEmailVerificationTokens: any; unverifyEmail: any; verifyEmailUsingToken: any }

    Type declaration

    • createEmailVerificationToken:function
      • createEmailVerificationToken(input: { email: string; recipeUserId: RecipeUserId; tenantId: string; userContext: UserContext }): Promise<{ status: "OK"; token: string } | { status: "EMAIL_ALREADY_VERIFIED_ERROR" }>
    • isEmailVerified:function
      • isEmailVerified(input: { email: string; recipeUserId: RecipeUserId; userContext: UserContext }): Promise<boolean>
    • revokeEmailVerificationTokens:function
      • revokeEmailVerificationTokens(input: { email: string; recipeUserId: RecipeUserId; tenantId: string; userContext: UserContext }): Promise<{ status: "OK" }>
    • unverifyEmail:function
      • unverifyEmail(input: { email: string; recipeUserId: RecipeUserId; userContext: UserContext }): Promise<{ status: "OK" }>
    • verifyEmailUsingToken:function
      • verifyEmailUsingToken(input: { attemptAccountLinking: boolean; tenantId: string; token: string; userContext: UserContext }): Promise<{ status: "OK"; user: UserEmailInfo } | { status: "EMAIL_VERIFICATION_INVALID_TOKEN_ERROR" }>
      • Parameters

        • input: { attemptAccountLinking: boolean; tenantId: string; token: string; userContext: UserContext }
          • attemptAccountLinking: boolean
          • tenantId: string
          • token: string
          • userContext: UserContext

        Returns Promise<{ status: "OK"; user: UserEmailInfo } | { status: "EMAIL_VERIFICATION_INVALID_TOKEN_ERROR" }>

    UserEmailInfo: { email: string; recipeUserId: RecipeUserId }

    Type declaration

    Variables

    EmailVerificationClaim: EmailVerificationClaimClass = ...
    Error: typeof default = Wrapper.Error

    Functions

    • createEmailVerificationLink(tenantId: string, recipeUserId: RecipeUserId, email?: string, userContext?: Record<string, any>): Promise<{ link: string; status: "OK" } | { status: "EMAIL_ALREADY_VERIFIED_ERROR" }>
    • createEmailVerificationToken(tenantId: string, recipeUserId: RecipeUserId, email?: string, userContext?: Record<string, any>): Promise<{ status: "OK"; token: string } | { status: "EMAIL_ALREADY_VERIFIED_ERROR" }>
    • init(config: TypeInput): RecipeListFunction
    • isEmailVerified(recipeUserId: RecipeUserId, email?: string, userContext?: Record<string, any>): Promise<boolean>
    • revokeEmailVerificationTokens(tenantId: string, recipeUserId: RecipeUserId, email?: string, userContext?: Record<string, any>): Promise<{ status: string }>
    • sendEmail(input: TypeEmailVerificationEmailDeliveryInput & { userContext?: Record<string, any> }): Promise<void>
    • sendEmailVerificationEmail(tenantId: string, userId: string, recipeUserId: RecipeUserId, email?: string, userContext?: Record<string, any>): Promise<{ status: "OK" } | { status: "EMAIL_ALREADY_VERIFIED_ERROR" }>
    • unverifyEmail(recipeUserId: RecipeUserId, email?: string, userContext?: Record<string, any>): Promise<{ status: string }>
    • verifyEmailUsingToken(tenantId: string, token: string, attemptAccountLinking?: boolean, userContext?: Record<string, any>): Promise<{ status: "OK"; user: UserEmailInfo } | { status: "EMAIL_VERIFICATION_INVALID_TOKEN_ERROR" }>

    Legend

    • Variable
    • Function
    • Function with type parameter
    • Type alias
    • Type alias with type parameter
    • Class
    • Class with type parameter
    • Interface

    Settings

    Theme

    Generated using TypeDoc

    \ No newline at end of file diff --git a/docs/modules/recipe_jwt.html b/docs/modules/recipe_jwt.html index 8451f9156..b8195e3ab 100644 --- a/docs/modules/recipe_jwt.html +++ b/docs/modules/recipe_jwt.html @@ -1 +1 @@ -recipe/jwt | supertokens-node
    Options
    All
    • Public
    • Public/Protected
    • All
    Menu

    Module recipe/jwt

    Index

    Type Aliases

    APIInterface: { getJWKSGET: undefined | ((input: { options: APIOptions; userContext: UserContext }) => Promise<{ keys: JsonWebKey[] } | GeneralErrorResponse>) }

    Type declaration

    • getJWKSGET: undefined | ((input: { options: APIOptions; userContext: UserContext }) => Promise<{ keys: JsonWebKey[] } | GeneralErrorResponse>)
    APIOptions: { config: TypeNormalisedInput; isInServerlessEnv: boolean; recipeId: string; recipeImplementation: RecipeInterface; req: BaseRequest; res: BaseResponse }

    Type declaration

    JsonWebKey: { alg: string; e: string; kid: string; kty: string; n: string; use: string }

    Type declaration

    • alg: string
    • e: string
    • kid: string
    • kty: string
    • n: string
    • use: string
    RecipeInterface: { createJWT: any; getJWKS: any }

    Type declaration

    • createJWT:function
      • createJWT(input: { payload?: any; useStaticSigningKey?: boolean; userContext: UserContext; validitySeconds?: number }): Promise<{ jwt: string; status: "OK" } | { status: "UNSUPPORTED_ALGORITHM_ERROR" }>
      • Parameters

        • input: { payload?: any; useStaticSigningKey?: boolean; userContext: UserContext; validitySeconds?: number }
          • Optional payload?: any
          • Optional useStaticSigningKey?: boolean
          • userContext: UserContext
          • Optional validitySeconds?: number

        Returns Promise<{ jwt: string; status: "OK" } | { status: "UNSUPPORTED_ALGORITHM_ERROR" }>

    • getJWKS:function
      • getJWKS(input: { userContext: UserContext }): Promise<{ keys: JsonWebKey[]; validityInSeconds?: number }>

    Functions

    • createJWT(payload: any, validitySeconds?: number, useStaticSigningKey?: boolean, userContext?: Record<string, any>): Promise<{ jwt: string; status: "OK" } | { status: "UNSUPPORTED_ALGORITHM_ERROR" }>
    • Parameters

      • payload: any
      • Optional validitySeconds: number
      • Optional useStaticSigningKey: boolean
      • Optional userContext: Record<string, any>

      Returns Promise<{ jwt: string; status: "OK" } | { status: "UNSUPPORTED_ALGORITHM_ERROR" }>

    • getJWKS(userContext?: Record<string, any>): Promise<{ keys: JsonWebKey[]; validityInSeconds?: number }>
    • init(config?: TypeInput): RecipeListFunction

    Legend

    • Variable
    • Function
    • Function with type parameter
    • Type alias
    • Type alias with type parameter
    • Class
    • Class with type parameter
    • Interface

    Settings

    Theme

    Generated using TypeDoc

    \ No newline at end of file +recipe/jwt | supertokens-node
    Options
    All
    • Public
    • Public/Protected
    • All
    Menu

    Module recipe/jwt

    Index

    Type Aliases

    APIInterface: { getJWKSGET: undefined | ((input: { options: APIOptions; userContext: UserContext }) => Promise<{ keys: JsonWebKey[] } | GeneralErrorResponse>) }

    Type declaration

    • getJWKSGET: undefined | ((input: { options: APIOptions; userContext: UserContext }) => Promise<{ keys: JsonWebKey[] } | GeneralErrorResponse>)
    APIOptions: { config: TypeNormalisedInput; isInServerlessEnv: boolean; recipeId: string; recipeImplementation: RecipeInterface; req: BaseRequest; res: BaseResponse }

    Type declaration

    JsonWebKey: { alg: string; e: string; kid: string; kty: string; n: string; use: string }

    Type declaration

    • alg: string
    • e: string
    • kid: string
    • kty: string
    • n: string
    • use: string
    RecipeInterface: { createJWT: any; getJWKS: any }

    Type declaration

    • createJWT:function
      • createJWT(input: { payload?: any; useStaticSigningKey?: boolean; userContext: UserContext; validitySeconds?: number }): Promise<{ jwt: string; status: "OK" } | { status: "UNSUPPORTED_ALGORITHM_ERROR" }>
      • Parameters

        • input: { payload?: any; useStaticSigningKey?: boolean; userContext: UserContext; validitySeconds?: number }
          • Optional payload?: any
          • Optional useStaticSigningKey?: boolean
          • userContext: UserContext
          • Optional validitySeconds?: number

        Returns Promise<{ jwt: string; status: "OK" } | { status: "UNSUPPORTED_ALGORITHM_ERROR" }>

    • getJWKS:function
      • getJWKS(input: { userContext: UserContext }): Promise<{ keys: JsonWebKey[]; validityInSeconds?: number }>

    Functions

    • createJWT(payload: any, validitySeconds?: number, useStaticSigningKey?: boolean, userContext?: Record<string, any>): Promise<{ jwt: string; status: "OK" } | { status: "UNSUPPORTED_ALGORITHM_ERROR" }>
    • Parameters

      • payload: any
      • Optional validitySeconds: number
      • Optional useStaticSigningKey: boolean
      • Optional userContext: Record<string, any>

      Returns Promise<{ jwt: string; status: "OK" } | { status: "UNSUPPORTED_ALGORITHM_ERROR" }>

    • getJWKS(userContext?: Record<string, any>): Promise<{ keys: JsonWebKey[]; validityInSeconds?: number }>
    • init(config?: TypeInput): RecipeListFunction

    Legend

    • Variable
    • Function
    • Function with type parameter
    • Type alias
    • Type alias with type parameter
    • Class
    • Class with type parameter
    • Interface

    Settings

    Theme

    Generated using TypeDoc

    \ No newline at end of file diff --git a/docs/modules/recipe_multifactorauth.html b/docs/modules/recipe_multifactorauth.html index fba986690..38df48a22 100644 --- a/docs/modules/recipe_multifactorauth.html +++ b/docs/modules/recipe_multifactorauth.html @@ -1 +1 @@ -recipe/multifactorauth | supertokens-node
    Options
    All
    • Public
    • Public/Protected
    • All
    Menu

    Module recipe/multifactorauth

    Index

    Type Aliases

    APIInterface: { resyncSessionAndFetchMFAInfoPUT: undefined | ((input: { options: APIOptions; session: SessionContainer; userContext: UserContext }) => Promise<{ emails: Record<string, string[] | undefined>; factors: { allowedToSetup: string[]; alreadySetup: string[]; next: string[] }; phoneNumbers: Record<string, string[] | undefined>; status: "OK" } | GeneralErrorResponse>) }

    Type declaration

    • resyncSessionAndFetchMFAInfoPUT: undefined | ((input: { options: APIOptions; session: SessionContainer; userContext: UserContext }) => Promise<{ emails: Record<string, string[] | undefined>; factors: { allowedToSetup: string[]; alreadySetup: string[]; next: string[] }; phoneNumbers: Record<string, string[] | undefined>; status: "OK" } | GeneralErrorResponse>)
    APIOptions: { config: TypeNormalisedInput; isInServerlessEnv: boolean; recipeId: string; recipeImplementation: RecipeInterface; recipeInstance: Recipe; req: BaseRequest; res: BaseResponse }

    Type declaration

    RecipeInterface: { addToRequiredSecondaryFactorsForUser: any; assertAllowedToSetupFactorElseThrowInvalidClaimError: any; getFactorsSetupForUser: any; getMFARequirementsForAuth: any; getRequiredSecondaryFactorsForUser: any; markFactorAsCompleteInSession: any; removeFromRequiredSecondaryFactorsForUser: any }

    Type declaration

    • addToRequiredSecondaryFactorsForUser:function
      • addToRequiredSecondaryFactorsForUser(input: { factorId: string; userContext: UserContext; userId: string }): Promise<void>
      • Parameters

        • input: { factorId: string; userContext: UserContext; userId: string }
          • factorId: string
          • userContext: UserContext
          • userId: string

        Returns Promise<void>

    • assertAllowedToSetupFactorElseThrowInvalidClaimError:function
      • assertAllowedToSetupFactorElseThrowInvalidClaimError(input: { factorId: string; factorsSetUpForUser: Promise<string[]>; mfaRequirementsForAuth: Promise<MFARequirementList>; session: SessionContainer; userContext: UserContext }): Promise<void>
      • Parameters

        • input: { factorId: string; factorsSetUpForUser: Promise<string[]>; mfaRequirementsForAuth: Promise<MFARequirementList>; session: SessionContainer; userContext: UserContext }
          • factorId: string
          • factorsSetUpForUser: Promise<string[]>
          • mfaRequirementsForAuth: Promise<MFARequirementList>
          • session: SessionContainer
          • userContext: UserContext

        Returns Promise<void>

    • getFactorsSetupForUser:function
      • getFactorsSetupForUser(input: { user: User; userContext: UserContext }): Promise<string[]>
    • getMFARequirementsForAuth:function
      • getMFARequirementsForAuth(input: { accessTokenPayload: JSONObject; completedFactors: MFAClaimValue["c"]; factorsSetUpForUser: Promise<string[]>; requiredSecondaryFactorsForTenant: Promise<string[]>; requiredSecondaryFactorsForUser: Promise<string[]>; tenantId: string; user: Promise<User>; userContext: UserContext }): MFARequirementList | Promise<MFARequirementList>
      • Parameters

        • input: { accessTokenPayload: JSONObject; completedFactors: MFAClaimValue["c"]; factorsSetUpForUser: Promise<string[]>; requiredSecondaryFactorsForTenant: Promise<string[]>; requiredSecondaryFactorsForUser: Promise<string[]>; tenantId: string; user: Promise<User>; userContext: UserContext }
          • accessTokenPayload: JSONObject
          • completedFactors: MFAClaimValue["c"]
          • factorsSetUpForUser: Promise<string[]>
          • requiredSecondaryFactorsForTenant: Promise<string[]>
          • requiredSecondaryFactorsForUser: Promise<string[]>
          • tenantId: string
          • user: Promise<User>
          • userContext: UserContext

        Returns MFARequirementList | Promise<MFARequirementList>

    • getRequiredSecondaryFactorsForUser:function
      • getRequiredSecondaryFactorsForUser(input: { userContext: UserContext; userId: string }): Promise<string[]>
    • markFactorAsCompleteInSession:function
      • markFactorAsCompleteInSession(input: { factorId: string; session: SessionContainer; userContext: UserContext }): Promise<void>
    • removeFromRequiredSecondaryFactorsForUser:function
      • removeFromRequiredSecondaryFactorsForUser(input: { factorId: string; userContext: UserContext; userId: string }): Promise<void>

    Variables

    FactorIds: { EMAILPASSWORD: string; LINK_EMAIL: string; LINK_PHONE: string; OTP_EMAIL: string; OTP_PHONE: string; THIRDPARTY: string; TOTP: string } = ...

    Type declaration

    • EMAILPASSWORD: string
    • LINK_EMAIL: string
    • LINK_PHONE: string
    • OTP_EMAIL: string
    • OTP_PHONE: string
    • THIRDPARTY: string
    • TOTP: string
    MultiFactorAuthClaim: MultiFactorAuthClaimClass = ...

    Functions

    • addToRequiredSecondaryFactorsForUser(userId: string, factorId: string, userContext?: Record<string, any>): Promise<void>
    • assertAllowedToSetupFactorElseThrowInvalidClaimError(session: SessionContainer, factorId: string, userContext?: Record<string, any>): Promise<void>
    • getFactorsSetupForUser(userId: string, userContext?: Record<string, any>): Promise<string[]>
    • getMFARequirementsForAuth(session: SessionContainer, userContext?: Record<string, any>): Promise<MFARequirementList>
    • getRequiredSecondaryFactorsForUser(userId: string, userContext?: Record<string, any>): Promise<string[]>
    • init(config?: TypeInput): RecipeListFunction
    • markFactorAsCompleteInSession(session: SessionContainer, factorId: string, userContext?: Record<string, any>): Promise<void>
    • removeFromRequiredSecondaryFactorsForUser(userId: string, factorId: string, userContext?: Record<string, any>): Promise<void>

    Legend

    • Variable
    • Function
    • Function with type parameter
    • Type alias
    • Type alias with type parameter
    • Class
    • Class with type parameter
    • Interface

    Settings

    Theme

    Generated using TypeDoc

    \ No newline at end of file +recipe/multifactorauth | supertokens-node
    Options
    All
    • Public
    • Public/Protected
    • All
    Menu

    Module recipe/multifactorauth

    Index

    Type Aliases

    APIInterface: { resyncSessionAndFetchMFAInfoPUT: undefined | ((input: { options: APIOptions; session: SessionContainer; userContext: UserContext }) => Promise<{ emails: Record<string, string[] | undefined>; factors: { allowedToSetup: string[]; alreadySetup: string[]; next: string[] }; phoneNumbers: Record<string, string[] | undefined>; status: "OK" } | GeneralErrorResponse>) }

    Type declaration

    • resyncSessionAndFetchMFAInfoPUT: undefined | ((input: { options: APIOptions; session: SessionContainer; userContext: UserContext }) => Promise<{ emails: Record<string, string[] | undefined>; factors: { allowedToSetup: string[]; alreadySetup: string[]; next: string[] }; phoneNumbers: Record<string, string[] | undefined>; status: "OK" } | GeneralErrorResponse>)
    APIOptions: { config: TypeNormalisedInput; isInServerlessEnv: boolean; recipeId: string; recipeImplementation: RecipeInterface; recipeInstance: Recipe; req: BaseRequest; res: BaseResponse }

    Type declaration

    RecipeInterface: { addToRequiredSecondaryFactorsForUser: any; assertAllowedToSetupFactorElseThrowInvalidClaimError: any; getFactorsSetupForUser: any; getMFARequirementsForAuth: any; getRequiredSecondaryFactorsForUser: any; markFactorAsCompleteInSession: any; removeFromRequiredSecondaryFactorsForUser: any }

    Type declaration

    • addToRequiredSecondaryFactorsForUser:function
      • addToRequiredSecondaryFactorsForUser(input: { factorId: string; userContext: UserContext; userId: string }): Promise<void>
      • Parameters

        • input: { factorId: string; userContext: UserContext; userId: string }
          • factorId: string
          • userContext: UserContext
          • userId: string

        Returns Promise<void>

    • assertAllowedToSetupFactorElseThrowInvalidClaimError:function
      • assertAllowedToSetupFactorElseThrowInvalidClaimError(input: { factorId: string; factorsSetUpForUser: Promise<string[]>; mfaRequirementsForAuth: Promise<MFARequirementList>; session: SessionContainer; userContext: UserContext }): Promise<void>
      • Parameters

        • input: { factorId: string; factorsSetUpForUser: Promise<string[]>; mfaRequirementsForAuth: Promise<MFARequirementList>; session: SessionContainer; userContext: UserContext }
          • factorId: string
          • factorsSetUpForUser: Promise<string[]>
          • mfaRequirementsForAuth: Promise<MFARequirementList>
          • session: SessionContainer
          • userContext: UserContext

        Returns Promise<void>

    • getFactorsSetupForUser:function
      • getFactorsSetupForUser(input: { user: User; userContext: UserContext }): Promise<string[]>
    • getMFARequirementsForAuth:function
      • getMFARequirementsForAuth(input: { accessTokenPayload: JSONObject; completedFactors: MFAClaimValue["c"]; factorsSetUpForUser: Promise<string[]>; requiredSecondaryFactorsForTenant: Promise<string[]>; requiredSecondaryFactorsForUser: Promise<string[]>; tenantId: string; user: Promise<User>; userContext: UserContext }): MFARequirementList | Promise<MFARequirementList>
      • Parameters

        • input: { accessTokenPayload: JSONObject; completedFactors: MFAClaimValue["c"]; factorsSetUpForUser: Promise<string[]>; requiredSecondaryFactorsForTenant: Promise<string[]>; requiredSecondaryFactorsForUser: Promise<string[]>; tenantId: string; user: Promise<User>; userContext: UserContext }
          • accessTokenPayload: JSONObject
          • completedFactors: MFAClaimValue["c"]
          • factorsSetUpForUser: Promise<string[]>
          • requiredSecondaryFactorsForTenant: Promise<string[]>
          • requiredSecondaryFactorsForUser: Promise<string[]>
          • tenantId: string
          • user: Promise<User>
          • userContext: UserContext

        Returns MFARequirementList | Promise<MFARequirementList>

    • getRequiredSecondaryFactorsForUser:function
      • getRequiredSecondaryFactorsForUser(input: { userContext: UserContext; userId: string }): Promise<string[]>
    • markFactorAsCompleteInSession:function
      • markFactorAsCompleteInSession(input: { factorId: string; session: SessionContainer; userContext: UserContext }): Promise<void>
    • removeFromRequiredSecondaryFactorsForUser:function
      • removeFromRequiredSecondaryFactorsForUser(input: { factorId: string; userContext: UserContext; userId: string }): Promise<void>

    Variables

    FactorIds: { EMAILPASSWORD: string; LINK_EMAIL: string; LINK_PHONE: string; OTP_EMAIL: string; OTP_PHONE: string; THIRDPARTY: string; TOTP: string } = ...

    Type declaration

    • EMAILPASSWORD: string
    • LINK_EMAIL: string
    • LINK_PHONE: string
    • OTP_EMAIL: string
    • OTP_PHONE: string
    • THIRDPARTY: string
    • TOTP: string
    MultiFactorAuthClaim: MultiFactorAuthClaimClass = ...

    Functions

    • addToRequiredSecondaryFactorsForUser(userId: string, factorId: string, userContext?: Record<string, any>): Promise<void>
    • assertAllowedToSetupFactorElseThrowInvalidClaimError(session: SessionContainer, factorId: string, userContext?: Record<string, any>): Promise<void>
    • getFactorsSetupForUser(userId: string, userContext?: Record<string, any>): Promise<string[]>
    • getMFARequirementsForAuth(session: SessionContainer, userContext?: Record<string, any>): Promise<MFARequirementList>
    • getRequiredSecondaryFactorsForUser(userId: string, userContext?: Record<string, any>): Promise<string[]>
    • init(config?: TypeInput): RecipeListFunction
    • markFactorAsCompleteInSession(session: SessionContainer, factorId: string, userContext?: Record<string, any>): Promise<void>
    • removeFromRequiredSecondaryFactorsForUser(userId: string, factorId: string, userContext?: Record<string, any>): Promise<void>

    Legend

    • Variable
    • Function
    • Function with type parameter
    • Type alias
    • Type alias with type parameter
    • Class
    • Class with type parameter
    • Interface

    Settings

    Theme

    Generated using TypeDoc

    \ No newline at end of file diff --git a/docs/modules/recipe_multitenancy.html b/docs/modules/recipe_multitenancy.html index dc57c68f8..1960f9e27 100644 --- a/docs/modules/recipe_multitenancy.html +++ b/docs/modules/recipe_multitenancy.html @@ -1 +1 @@ -recipe/multitenancy | supertokens-node
    Options
    All
    • Public
    • Public/Protected
    • All
    Menu

    Module recipe/multitenancy

    Index

    Type Aliases

    APIInterface: { loginMethodsGET: any }

    Type declaration

    • loginMethodsGET:function
      • loginMethodsGET(input: { clientType?: string; options: APIOptions; tenantId: string; userContext: UserContext }): Promise<GeneralErrorResponse | { emailPassword: { enabled: boolean }; firstFactors: string[]; passwordless: { enabled: boolean }; status: "OK"; thirdParty: { enabled: boolean; providers: { id: string; name?: string }[] } }>
      • Parameters

        • input: { clientType?: string; options: APIOptions; tenantId: string; userContext: UserContext }
          • Optional clientType?: string
          • options: APIOptions
          • tenantId: string
          • userContext: UserContext

        Returns Promise<GeneralErrorResponse | { emailPassword: { enabled: boolean }; firstFactors: string[]; passwordless: { enabled: boolean }; status: "OK"; thirdParty: { enabled: boolean; providers: { id: string; name?: string }[] } }>

    APIOptions: { allAvailableFirstFactors: string[]; config: TypeNormalisedInput; isInServerlessEnv: boolean; recipeId: string; recipeImplementation: RecipeInterface; req: BaseRequest; res: BaseResponse; staticFirstFactors: string[] | undefined; staticThirdPartyProviders: ProviderInput[] }

    Type declaration

    • allAvailableFirstFactors: string[]
    • config: TypeNormalisedInput
    • isInServerlessEnv: boolean
    • recipeId: string
    • recipeImplementation: RecipeInterface
    • req: BaseRequest
    • res: BaseResponse
    • staticFirstFactors: string[] | undefined
    • staticThirdPartyProviders: ProviderInput[]
    RecipeInterface: { associateUserToTenant: any; createOrUpdateTenant: any; createOrUpdateThirdPartyConfig: any; deleteTenant: any; deleteThirdPartyConfig: any; disassociateUserFromTenant: any; getTenant: any; getTenantId: any; listAllTenants: any }

    Type declaration

    • associateUserToTenant:function
      • associateUserToTenant(input: { recipeUserId: RecipeUserId; tenantId: string; userContext: UserContext }): Promise<{ status: "OK"; wasAlreadyAssociated: boolean } | { status: "UNKNOWN_USER_ID_ERROR" | "EMAIL_ALREADY_EXISTS_ERROR" | "PHONE_NUMBER_ALREADY_EXISTS_ERROR" | "THIRD_PARTY_USER_ALREADY_EXISTS_ERROR" } | { reason: string; status: "ASSOCIATION_NOT_ALLOWED_ERROR" }>
      • Parameters

        • input: { recipeUserId: RecipeUserId; tenantId: string; userContext: UserContext }
          • recipeUserId: RecipeUserId
          • tenantId: string
          • userContext: UserContext

        Returns Promise<{ status: "OK"; wasAlreadyAssociated: boolean } | { status: "UNKNOWN_USER_ID_ERROR" | "EMAIL_ALREADY_EXISTS_ERROR" | "PHONE_NUMBER_ALREADY_EXISTS_ERROR" | "THIRD_PARTY_USER_ALREADY_EXISTS_ERROR" } | { reason: string; status: "ASSOCIATION_NOT_ALLOWED_ERROR" }>

    • createOrUpdateTenant:function
      • createOrUpdateTenant(input: { config?: { coreConfig?: {}; firstFactors?: string[] | null; requiredSecondaryFactors?: string[] | null }; tenantId: string; userContext: UserContext }): Promise<{ createdNew: boolean; status: "OK" }>
      • Parameters

        • input: { config?: { coreConfig?: {}; firstFactors?: string[] | null; requiredSecondaryFactors?: string[] | null }; tenantId: string; userContext: UserContext }
          • Optional config?: { coreConfig?: {}; firstFactors?: string[] | null; requiredSecondaryFactors?: string[] | null }
            • Optional coreConfig?: {}
              • [key: string]: any
            • Optional firstFactors?: string[] | null
            • Optional requiredSecondaryFactors?: string[] | null
          • tenantId: string
          • userContext: UserContext

        Returns Promise<{ createdNew: boolean; status: "OK" }>

    • createOrUpdateThirdPartyConfig:function
      • createOrUpdateThirdPartyConfig(input: { config: ProviderConfig; skipValidation?: boolean; tenantId: string; userContext: UserContext }): Promise<{ createdNew: boolean; status: "OK" }>
      • Parameters

        • input: { config: ProviderConfig; skipValidation?: boolean; tenantId: string; userContext: UserContext }
          • config: ProviderConfig
          • Optional skipValidation?: boolean
          • tenantId: string
          • userContext: UserContext

        Returns Promise<{ createdNew: boolean; status: "OK" }>

    • deleteTenant:function
      • deleteTenant(input: { tenantId: string; userContext: UserContext }): Promise<{ didExist: boolean; status: "OK" }>
      • Parameters

        • input: { tenantId: string; userContext: UserContext }
          • tenantId: string
          • userContext: UserContext

        Returns Promise<{ didExist: boolean; status: "OK" }>

    • deleteThirdPartyConfig:function
      • deleteThirdPartyConfig(input: { tenantId: string; thirdPartyId: string; userContext: UserContext }): Promise<{ didConfigExist: boolean; status: "OK" }>
      • Parameters

        • input: { tenantId: string; thirdPartyId: string; userContext: UserContext }
          • tenantId: string
          • thirdPartyId: string
          • userContext: UserContext

        Returns Promise<{ didConfigExist: boolean; status: "OK" }>

    • disassociateUserFromTenant:function
      • disassociateUserFromTenant(input: { recipeUserId: RecipeUserId; tenantId: string; userContext: UserContext }): Promise<{ status: "OK"; wasAssociated: boolean }>
    • getTenant:function
      • getTenant(input: { tenantId: string; userContext: UserContext }): Promise<undefined | { status: "OK" } & TenantConfig>
      • Parameters

        • input: { tenantId: string; userContext: UserContext }
          • tenantId: string
          • userContext: UserContext

        Returns Promise<undefined | { status: "OK" } & TenantConfig>

    • getTenantId:function
      • getTenantId(input: { tenantIdFromFrontend: string; userContext: UserContext }): Promise<string>
      • Parameters

        • input: { tenantIdFromFrontend: string; userContext: UserContext }
          • tenantIdFromFrontend: string
          • userContext: UserContext

        Returns Promise<string>

    • listAllTenants:function
      • listAllTenants(input: { userContext: UserContext }): Promise<{ status: "OK"; tenants: (TenantConfig & { tenantId: string })[] }>
      • Parameters

        • input: { userContext: UserContext }
          • userContext: UserContext

        Returns Promise<{ status: "OK"; tenants: (TenantConfig & { tenantId: string })[] }>

    Variables

    AllowedDomainsClaim: AllowedDomainsClaimClass = ...

    Functions

    • associateUserToTenant(tenantId: string, recipeUserId: RecipeUserId, userContext?: Record<string, any>): Promise<{ status: "OK"; wasAlreadyAssociated: boolean } | { status: "EMAIL_ALREADY_EXISTS_ERROR" | "UNKNOWN_USER_ID_ERROR" | "PHONE_NUMBER_ALREADY_EXISTS_ERROR" | "THIRD_PARTY_USER_ALREADY_EXISTS_ERROR" } | { reason: string; status: "ASSOCIATION_NOT_ALLOWED_ERROR" }>
    • Parameters

      • tenantId: string
      • recipeUserId: RecipeUserId
      • Optional userContext: Record<string, any>

      Returns Promise<{ status: "OK"; wasAlreadyAssociated: boolean } | { status: "EMAIL_ALREADY_EXISTS_ERROR" | "UNKNOWN_USER_ID_ERROR" | "PHONE_NUMBER_ALREADY_EXISTS_ERROR" | "THIRD_PARTY_USER_ALREADY_EXISTS_ERROR" } | { reason: string; status: "ASSOCIATION_NOT_ALLOWED_ERROR" }>

    • createOrUpdateTenant(tenantId: string, config?: { coreConfig?: {}; firstFactors?: null | string[]; requiredSecondaryFactors?: null | string[] }, userContext?: Record<string, any>): Promise<{ createdNew: boolean; status: "OK" }>
    • Parameters

      • tenantId: string
      • Optional config: { coreConfig?: {}; firstFactors?: null | string[]; requiredSecondaryFactors?: null | string[] }
        • Optional coreConfig?: {}
          • [key: string]: any
        • Optional firstFactors?: null | string[]
        • Optional requiredSecondaryFactors?: null | string[]
      • Optional userContext: Record<string, any>

      Returns Promise<{ createdNew: boolean; status: "OK" }>

    • createOrUpdateThirdPartyConfig(tenantId: string, config: ProviderConfig, skipValidation?: boolean, userContext?: Record<string, any>): Promise<{ createdNew: boolean; status: "OK" }>
    • Parameters

      • tenantId: string
      • config: ProviderConfig
      • Optional skipValidation: boolean
      • Optional userContext: Record<string, any>

      Returns Promise<{ createdNew: boolean; status: "OK" }>

    • deleteTenant(tenantId: string, userContext?: Record<string, any>): Promise<{ didExist: boolean; status: "OK" }>
    • deleteThirdPartyConfig(tenantId: string, thirdPartyId: string, userContext?: Record<string, any>): Promise<{ didConfigExist: boolean; status: "OK" }>
    • Parameters

      • tenantId: string
      • thirdPartyId: string
      • Optional userContext: Record<string, any>

      Returns Promise<{ didConfigExist: boolean; status: "OK" }>

    • disassociateUserFromTenant(tenantId: string, recipeUserId: RecipeUserId, userContext?: Record<string, any>): Promise<{ status: "OK"; wasAssociated: boolean }>
    • getTenant(tenantId: string, userContext?: Record<string, any>): Promise<undefined | { status: "OK" } & TenantConfig>
    • init(config?: TypeInput): RecipeListFunction
    • listAllTenants(userContext?: Record<string, any>): Promise<{ status: "OK"; tenants: ({ tenantId: string } & TenantConfig)[] }>

    Legend

    • Variable
    • Function
    • Function with type parameter
    • Type alias
    • Type alias with type parameter
    • Class
    • Class with type parameter
    • Interface

    Settings

    Theme

    Generated using TypeDoc

    \ No newline at end of file +recipe/multitenancy | supertokens-node
    Options
    All
    • Public
    • Public/Protected
    • All
    Menu

    Module recipe/multitenancy

    Index

    Type Aliases

    APIInterface: { loginMethodsGET: any }

    Type declaration

    • loginMethodsGET:function
      • loginMethodsGET(input: { clientType?: string; options: APIOptions; tenantId: string; userContext: UserContext }): Promise<GeneralErrorResponse | { emailPassword: { enabled: boolean }; firstFactors: string[]; passwordless: { enabled: boolean }; status: "OK"; thirdParty: { enabled: boolean; providers: { id: string; name?: string }[] } }>
      • Parameters

        • input: { clientType?: string; options: APIOptions; tenantId: string; userContext: UserContext }
          • Optional clientType?: string
          • options: APIOptions
          • tenantId: string
          • userContext: UserContext

        Returns Promise<GeneralErrorResponse | { emailPassword: { enabled: boolean }; firstFactors: string[]; passwordless: { enabled: boolean }; status: "OK"; thirdParty: { enabled: boolean; providers: { id: string; name?: string }[] } }>

    APIOptions: { allAvailableFirstFactors: string[]; config: TypeNormalisedInput; isInServerlessEnv: boolean; recipeId: string; recipeImplementation: RecipeInterface; req: BaseRequest; res: BaseResponse; staticFirstFactors: string[] | undefined; staticThirdPartyProviders: ProviderInput[] }

    Type declaration

    • allAvailableFirstFactors: string[]
    • config: TypeNormalisedInput
    • isInServerlessEnv: boolean
    • recipeId: string
    • recipeImplementation: RecipeInterface
    • req: BaseRequest
    • res: BaseResponse
    • staticFirstFactors: string[] | undefined
    • staticThirdPartyProviders: ProviderInput[]
    RecipeInterface: { associateUserToTenant: any; createOrUpdateTenant: any; createOrUpdateThirdPartyConfig: any; deleteTenant: any; deleteThirdPartyConfig: any; disassociateUserFromTenant: any; getTenant: any; getTenantId: any; listAllTenants: any }

    Type declaration

    • associateUserToTenant:function
      • associateUserToTenant(input: { recipeUserId: RecipeUserId; tenantId: string; userContext: UserContext }): Promise<{ status: "OK"; wasAlreadyAssociated: boolean } | { status: "UNKNOWN_USER_ID_ERROR" | "EMAIL_ALREADY_EXISTS_ERROR" | "PHONE_NUMBER_ALREADY_EXISTS_ERROR" | "THIRD_PARTY_USER_ALREADY_EXISTS_ERROR" } | { reason: string; status: "ASSOCIATION_NOT_ALLOWED_ERROR" }>
      • Parameters

        • input: { recipeUserId: RecipeUserId; tenantId: string; userContext: UserContext }
          • recipeUserId: RecipeUserId
          • tenantId: string
          • userContext: UserContext

        Returns Promise<{ status: "OK"; wasAlreadyAssociated: boolean } | { status: "UNKNOWN_USER_ID_ERROR" | "EMAIL_ALREADY_EXISTS_ERROR" | "PHONE_NUMBER_ALREADY_EXISTS_ERROR" | "THIRD_PARTY_USER_ALREADY_EXISTS_ERROR" } | { reason: string; status: "ASSOCIATION_NOT_ALLOWED_ERROR" }>

    • createOrUpdateTenant:function
      • createOrUpdateTenant(input: { config?: { coreConfig?: {}; firstFactors?: string[] | null; requiredSecondaryFactors?: string[] | null }; tenantId: string; userContext: UserContext }): Promise<{ createdNew: boolean; status: "OK" }>
      • Parameters

        • input: { config?: { coreConfig?: {}; firstFactors?: string[] | null; requiredSecondaryFactors?: string[] | null }; tenantId: string; userContext: UserContext }
          • Optional config?: { coreConfig?: {}; firstFactors?: string[] | null; requiredSecondaryFactors?: string[] | null }
            • Optional coreConfig?: {}
              • [key: string]: any
            • Optional firstFactors?: string[] | null
            • Optional requiredSecondaryFactors?: string[] | null
          • tenantId: string
          • userContext: UserContext

        Returns Promise<{ createdNew: boolean; status: "OK" }>

    • createOrUpdateThirdPartyConfig:function
      • createOrUpdateThirdPartyConfig(input: { config: ProviderConfig; skipValidation?: boolean; tenantId: string; userContext: UserContext }): Promise<{ createdNew: boolean; status: "OK" }>
      • Parameters

        • input: { config: ProviderConfig; skipValidation?: boolean; tenantId: string; userContext: UserContext }
          • config: ProviderConfig
          • Optional skipValidation?: boolean
          • tenantId: string
          • userContext: UserContext

        Returns Promise<{ createdNew: boolean; status: "OK" }>

    • deleteTenant:function
      • deleteTenant(input: { tenantId: string; userContext: UserContext }): Promise<{ didExist: boolean; status: "OK" }>
      • Parameters

        • input: { tenantId: string; userContext: UserContext }
          • tenantId: string
          • userContext: UserContext

        Returns Promise<{ didExist: boolean; status: "OK" }>

    • deleteThirdPartyConfig:function
      • deleteThirdPartyConfig(input: { tenantId: string; thirdPartyId: string; userContext: UserContext }): Promise<{ didConfigExist: boolean; status: "OK" }>
      • Parameters

        • input: { tenantId: string; thirdPartyId: string; userContext: UserContext }
          • tenantId: string
          • thirdPartyId: string
          • userContext: UserContext

        Returns Promise<{ didConfigExist: boolean; status: "OK" }>

    • disassociateUserFromTenant:function
      • disassociateUserFromTenant(input: { recipeUserId: RecipeUserId; tenantId: string; userContext: UserContext }): Promise<{ status: "OK"; wasAssociated: boolean }>
    • getTenant:function
      • getTenant(input: { tenantId: string; userContext: UserContext }): Promise<undefined | { status: "OK" } & TenantConfig>
      • Parameters

        • input: { tenantId: string; userContext: UserContext }
          • tenantId: string
          • userContext: UserContext

        Returns Promise<undefined | { status: "OK" } & TenantConfig>

    • getTenantId:function
      • getTenantId(input: { tenantIdFromFrontend: string; userContext: UserContext }): Promise<string>
      • Parameters

        • input: { tenantIdFromFrontend: string; userContext: UserContext }
          • tenantIdFromFrontend: string
          • userContext: UserContext

        Returns Promise<string>

    • listAllTenants:function
      • listAllTenants(input: { userContext: UserContext }): Promise<{ status: "OK"; tenants: (TenantConfig & { tenantId: string })[] }>
      • Parameters

        • input: { userContext: UserContext }
          • userContext: UserContext

        Returns Promise<{ status: "OK"; tenants: (TenantConfig & { tenantId: string })[] }>

    Variables

    AllowedDomainsClaim: AllowedDomainsClaimClass = ...

    Functions

    • associateUserToTenant(tenantId: string, recipeUserId: RecipeUserId, userContext?: Record<string, any>): Promise<{ status: "OK"; wasAlreadyAssociated: boolean } | { status: "EMAIL_ALREADY_EXISTS_ERROR" | "UNKNOWN_USER_ID_ERROR" | "PHONE_NUMBER_ALREADY_EXISTS_ERROR" | "THIRD_PARTY_USER_ALREADY_EXISTS_ERROR" } | { reason: string; status: "ASSOCIATION_NOT_ALLOWED_ERROR" }>
    • Parameters

      • tenantId: string
      • recipeUserId: RecipeUserId
      • Optional userContext: Record<string, any>

      Returns Promise<{ status: "OK"; wasAlreadyAssociated: boolean } | { status: "EMAIL_ALREADY_EXISTS_ERROR" | "UNKNOWN_USER_ID_ERROR" | "PHONE_NUMBER_ALREADY_EXISTS_ERROR" | "THIRD_PARTY_USER_ALREADY_EXISTS_ERROR" } | { reason: string; status: "ASSOCIATION_NOT_ALLOWED_ERROR" }>

    • createOrUpdateTenant(tenantId: string, config?: { coreConfig?: {}; firstFactors?: null | string[]; requiredSecondaryFactors?: null | string[] }, userContext?: Record<string, any>): Promise<{ createdNew: boolean; status: "OK" }>
    • Parameters

      • tenantId: string
      • Optional config: { coreConfig?: {}; firstFactors?: null | string[]; requiredSecondaryFactors?: null | string[] }
        • Optional coreConfig?: {}
          • [key: string]: any
        • Optional firstFactors?: null | string[]
        • Optional requiredSecondaryFactors?: null | string[]
      • Optional userContext: Record<string, any>

      Returns Promise<{ createdNew: boolean; status: "OK" }>

    • createOrUpdateThirdPartyConfig(tenantId: string, config: ProviderConfig, skipValidation?: boolean, userContext?: Record<string, any>): Promise<{ createdNew: boolean; status: "OK" }>
    • Parameters

      • tenantId: string
      • config: ProviderConfig
      • Optional skipValidation: boolean
      • Optional userContext: Record<string, any>

      Returns Promise<{ createdNew: boolean; status: "OK" }>

    • deleteTenant(tenantId: string, userContext?: Record<string, any>): Promise<{ didExist: boolean; status: "OK" }>
    • deleteThirdPartyConfig(tenantId: string, thirdPartyId: string, userContext?: Record<string, any>): Promise<{ didConfigExist: boolean; status: "OK" }>
    • Parameters

      • tenantId: string
      • thirdPartyId: string
      • Optional userContext: Record<string, any>

      Returns Promise<{ didConfigExist: boolean; status: "OK" }>

    • disassociateUserFromTenant(tenantId: string, recipeUserId: RecipeUserId, userContext?: Record<string, any>): Promise<{ status: "OK"; wasAssociated: boolean }>
    • getTenant(tenantId: string, userContext?: Record<string, any>): Promise<undefined | { status: "OK" } & TenantConfig>
    • init(config?: TypeInput): RecipeListFunction
    • listAllTenants(userContext?: Record<string, any>): Promise<{ status: "OK"; tenants: ({ tenantId: string } & TenantConfig)[] }>

    Legend

    • Variable
    • Function
    • Function with type parameter
    • Type alias
    • Type alias with type parameter
    • Class
    • Class with type parameter
    • Interface

    Settings

    Theme

    Generated using TypeDoc

    \ No newline at end of file diff --git a/docs/modules/recipe_openid.html b/docs/modules/recipe_openid.html index 29bd8beb1..7ea37707f 100644 --- a/docs/modules/recipe_openid.html +++ b/docs/modules/recipe_openid.html @@ -1 +1 @@ -recipe/openid | supertokens-node
    Options
    All
    • Public
    • Public/Protected
    • All
    Menu

    Module recipe/openid

    Index

    Functions

    • createJWT(payload?: any, validitySeconds?: number, useStaticSigningKey?: boolean, userContext?: Record<string, any>): Promise<{ jwt: string; status: "OK" } | { status: "UNSUPPORTED_ALGORITHM_ERROR" }>
    • Parameters

      • Optional payload: any
      • Optional validitySeconds: number
      • Optional useStaticSigningKey: boolean
      • Optional userContext: Record<string, any>

      Returns Promise<{ jwt: string; status: "OK" } | { status: "UNSUPPORTED_ALGORITHM_ERROR" }>

    • getJWKS(userContext?: Record<string, any>): Promise<{ keys: JsonWebKey[]; validityInSeconds?: number }>
    • getOpenIdDiscoveryConfiguration(userContext?: Record<string, any>): Promise<{ issuer: string; jwks_uri: string; status: "OK" }>
    • Parameters

      • Optional userContext: Record<string, any>

      Returns Promise<{ issuer: string; jwks_uri: string; status: "OK" }>

    • init(config?: TypeInput): RecipeListFunction

    Legend

    • Variable
    • Function
    • Function with type parameter
    • Type alias
    • Type alias with type parameter
    • Class
    • Class with type parameter
    • Interface

    Settings

    Theme

    Generated using TypeDoc

    \ No newline at end of file +recipe/openid | supertokens-node
    Options
    All
    • Public
    • Public/Protected
    • All
    Menu

    Module recipe/openid

    Index

    Functions

    • createJWT(payload?: any, validitySeconds?: number, useStaticSigningKey?: boolean, userContext?: Record<string, any>): Promise<{ jwt: string; status: "OK" } | { status: "UNSUPPORTED_ALGORITHM_ERROR" }>
    • Parameters

      • Optional payload: any
      • Optional validitySeconds: number
      • Optional useStaticSigningKey: boolean
      • Optional userContext: Record<string, any>

      Returns Promise<{ jwt: string; status: "OK" } | { status: "UNSUPPORTED_ALGORITHM_ERROR" }>

    • getJWKS(userContext?: Record<string, any>): Promise<{ keys: JsonWebKey[]; validityInSeconds?: number }>
    • getOpenIdDiscoveryConfiguration(userContext?: Record<string, any>): Promise<{ issuer: string; jwks_uri: string; status: "OK" }>
    • Parameters

      • Optional userContext: Record<string, any>

      Returns Promise<{ issuer: string; jwks_uri: string; status: "OK" }>

    • init(config?: TypeInput): RecipeListFunction

    Legend

    • Variable
    • Function
    • Function with type parameter
    • Type alias
    • Type alias with type parameter
    • Class
    • Class with type parameter
    • Interface

    Settings

    Theme

    Generated using TypeDoc

    \ No newline at end of file diff --git a/docs/modules/recipe_passwordless.html b/docs/modules/recipe_passwordless.html index 5f0d467ee..8c5157ab9 100644 --- a/docs/modules/recipe_passwordless.html +++ b/docs/modules/recipe_passwordless.html @@ -1 +1 @@ -recipe/passwordless | supertokens-node
    Options
    All
    • Public
    • Public/Protected
    • All
    Menu

    Module recipe/passwordless

    Index

    Type Aliases

    APIInterface: { consumeCodePOST?: any; createCodePOST?: any; emailExistsGET?: any; phoneNumberExistsGET?: any; resendCodePOST?: any }

    Type declaration

    • consumeCodePOST?:function
      • consumeCodePOST(input: { deviceId: string; preAuthSessionId: string; userInputCode: string } & { options: APIOptions; session: SessionContainer | undefined; tenantId: string; userContext: UserContext } & { linkCode: string; preAuthSessionId: string } & { options: APIOptions; session: SessionContainer | undefined; tenantId: string; userContext: UserContext }): Promise<GeneralErrorResponse | { createdNewRecipeUser: boolean; session: SessionContainer; status: "OK"; user: User } | { failedCodeInputAttemptCount: number; maximumCodeInputAttempts: number; status: "INCORRECT_USER_INPUT_CODE_ERROR" | "EXPIRED_USER_INPUT_CODE_ERROR" } | { status: "RESTART_FLOW_ERROR" } | { reason: string; status: "SIGN_IN_UP_NOT_ALLOWED" }>
      • Parameters

        • input: { deviceId: string; preAuthSessionId: string; userInputCode: string } & { options: APIOptions; session: SessionContainer | undefined; tenantId: string; userContext: UserContext } & { linkCode: string; preAuthSessionId: string } & { options: APIOptions; session: SessionContainer | undefined; tenantId: string; userContext: UserContext }

        Returns Promise<GeneralErrorResponse | { createdNewRecipeUser: boolean; session: SessionContainer; status: "OK"; user: User } | { failedCodeInputAttemptCount: number; maximumCodeInputAttempts: number; status: "INCORRECT_USER_INPUT_CODE_ERROR" | "EXPIRED_USER_INPUT_CODE_ERROR" } | { status: "RESTART_FLOW_ERROR" } | { reason: string; status: "SIGN_IN_UP_NOT_ALLOWED" }>

    • createCodePOST?:function
      • createCodePOST(input: { email: string } & { options: APIOptions; session: SessionContainer | undefined; tenantId: string; userContext: UserContext } & { phoneNumber: string } & { options: APIOptions; session: SessionContainer | undefined; tenantId: string; userContext: UserContext }): Promise<GeneralErrorResponse | { deviceId: string; flowType: "USER_INPUT_CODE" | "MAGIC_LINK" | "USER_INPUT_CODE_AND_MAGIC_LINK"; preAuthSessionId: string; status: "OK" } | { reason: string; status: "SIGN_IN_UP_NOT_ALLOWED" }>
      • Parameters

        • input: { email: string } & { options: APIOptions; session: SessionContainer | undefined; tenantId: string; userContext: UserContext } & { phoneNumber: string } & { options: APIOptions; session: SessionContainer | undefined; tenantId: string; userContext: UserContext }

        Returns Promise<GeneralErrorResponse | { deviceId: string; flowType: "USER_INPUT_CODE" | "MAGIC_LINK" | "USER_INPUT_CODE_AND_MAGIC_LINK"; preAuthSessionId: string; status: "OK" } | { reason: string; status: "SIGN_IN_UP_NOT_ALLOWED" }>

    • emailExistsGET?:function
      • emailExistsGET(input: { email: string; options: APIOptions; tenantId: string; userContext: UserContext }): Promise<GeneralErrorResponse | { exists: boolean; status: "OK" }>
    • phoneNumberExistsGET?:function
      • phoneNumberExistsGET(input: { options: APIOptions; phoneNumber: string; tenantId: string; userContext: UserContext }): Promise<GeneralErrorResponse | { exists: boolean; status: "OK" }>
      • Parameters

        • input: { options: APIOptions; phoneNumber: string; tenantId: string; userContext: UserContext }
          • options: APIOptions
          • phoneNumber: string
          • tenantId: string
          • userContext: UserContext

        Returns Promise<GeneralErrorResponse | { exists: boolean; status: "OK" }>

    • resendCodePOST?:function
      • resendCodePOST(input: { deviceId: string; preAuthSessionId: string } & { options: APIOptions; session: SessionContainer | undefined; tenantId: string; userContext: UserContext }): Promise<GeneralErrorResponse | { status: "RESTART_FLOW_ERROR" | "OK" }>
    APIOptions: { appInfo: NormalisedAppinfo; config: TypeNormalisedInput; emailDelivery: default<TypePasswordlessEmailDeliveryInput>; isInServerlessEnv: boolean; recipeId: string; recipeImplementation: RecipeInterface; req: BaseRequest; res: BaseResponse; smsDelivery: default<TypePasswordlessSmsDeliveryInput> }

    Type declaration

    • appInfo: NormalisedAppinfo
    • config: TypeNormalisedInput
    • emailDelivery: default<TypePasswordlessEmailDeliveryInput>
    • isInServerlessEnv: boolean
    • recipeId: string
    • recipeImplementation: RecipeInterface
    • req: BaseRequest
    • res: BaseResponse
    • smsDelivery: default<TypePasswordlessSmsDeliveryInput>
    RecipeInterface: { checkCode: any; consumeCode: any; createCode: any; createNewCodeForDevice: any; listCodesByDeviceId: any; listCodesByEmail: any; listCodesByPhoneNumber: any; listCodesByPreAuthSessionId: any; revokeAllCodes: any; revokeCode: any; updateUser: any }

    Type declaration

    • checkCode:function
      • checkCode(input: { deviceId: string; preAuthSessionId: string; tenantId: string; userContext: UserContext; userInputCode: string } | { linkCode: string; preAuthSessionId: string; tenantId: string; userContext: UserContext }): Promise<{ consumedDevice: { email?: string; failedCodeInputAttemptCount: number; phoneNumber?: string; preAuthSessionId: string }; status: "OK" } | { failedCodeInputAttemptCount: number; maximumCodeInputAttempts: number; status: "INCORRECT_USER_INPUT_CODE_ERROR" | "EXPIRED_USER_INPUT_CODE_ERROR" } | { status: "RESTART_FLOW_ERROR" }>
      • Parameters

        • input: { deviceId: string; preAuthSessionId: string; tenantId: string; userContext: UserContext; userInputCode: string } | { linkCode: string; preAuthSessionId: string; tenantId: string; userContext: UserContext }

        Returns Promise<{ consumedDevice: { email?: string; failedCodeInputAttemptCount: number; phoneNumber?: string; preAuthSessionId: string }; status: "OK" } | { failedCodeInputAttemptCount: number; maximumCodeInputAttempts: number; status: "INCORRECT_USER_INPUT_CODE_ERROR" | "EXPIRED_USER_INPUT_CODE_ERROR" } | { status: "RESTART_FLOW_ERROR" }>

    • consumeCode:function
      • consumeCode(input: { deviceId: string; preAuthSessionId: string; session: SessionContainer | undefined; tenantId: string; userContext: UserContext; userInputCode: string } | { linkCode: string; preAuthSessionId: string; session: SessionContainer | undefined; tenantId: string; userContext: UserContext }): Promise<{ consumedDevice: { email?: string; failedCodeInputAttemptCount: number; phoneNumber?: string; preAuthSessionId: string }; createdNewRecipeUser: boolean; recipeUserId: RecipeUserId; status: "OK"; user: User } | { failedCodeInputAttemptCount: number; maximumCodeInputAttempts: number; status: "INCORRECT_USER_INPUT_CODE_ERROR" | "EXPIRED_USER_INPUT_CODE_ERROR" } | { status: "RESTART_FLOW_ERROR" } | { reason: "EMAIL_VERIFICATION_REQUIRED" | "RECIPE_USER_ID_ALREADY_LINKED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR" | "ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR" | "SESSION_USER_ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR"; status: "LINKING_TO_SESSION_USER_FAILED" }>
      • Parameters

        • input: { deviceId: string; preAuthSessionId: string; session: SessionContainer | undefined; tenantId: string; userContext: UserContext; userInputCode: string } | { linkCode: string; preAuthSessionId: string; session: SessionContainer | undefined; tenantId: string; userContext: UserContext }

        Returns Promise<{ consumedDevice: { email?: string; failedCodeInputAttemptCount: number; phoneNumber?: string; preAuthSessionId: string }; createdNewRecipeUser: boolean; recipeUserId: RecipeUserId; status: "OK"; user: User } | { failedCodeInputAttemptCount: number; maximumCodeInputAttempts: number; status: "INCORRECT_USER_INPUT_CODE_ERROR" | "EXPIRED_USER_INPUT_CODE_ERROR" } | { status: "RESTART_FLOW_ERROR" } | { reason: "EMAIL_VERIFICATION_REQUIRED" | "RECIPE_USER_ID_ALREADY_LINKED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR" | "ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR" | "SESSION_USER_ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR"; status: "LINKING_TO_SESSION_USER_FAILED" }>

    • createCode:function
      • createCode(input: { email: string } & { session: SessionContainer | undefined; tenantId: string; userContext: UserContext; userInputCode?: string } & { phoneNumber: string } & { session: SessionContainer | undefined; tenantId: string; userContext: UserContext; userInputCode?: string }): Promise<{ codeId: string; codeLifetime: number; deviceId: string; linkCode: string; preAuthSessionId: string; status: "OK"; timeCreated: number; userInputCode: string }>
      • Parameters

        • input: { email: string } & { session: SessionContainer | undefined; tenantId: string; userContext: UserContext; userInputCode?: string } & { phoneNumber: string } & { session: SessionContainer | undefined; tenantId: string; userContext: UserContext; userInputCode?: string }

        Returns Promise<{ codeId: string; codeLifetime: number; deviceId: string; linkCode: string; preAuthSessionId: string; status: "OK"; timeCreated: number; userInputCode: string }>

    • createNewCodeForDevice:function
      • createNewCodeForDevice(input: { deviceId: string; tenantId: string; userContext: UserContext; userInputCode?: string }): Promise<{ codeId: string; codeLifetime: number; deviceId: string; linkCode: string; preAuthSessionId: string; status: "OK"; timeCreated: number; userInputCode: string } | { status: "RESTART_FLOW_ERROR" | "USER_INPUT_CODE_ALREADY_USED_ERROR" }>
      • Parameters

        • input: { deviceId: string; tenantId: string; userContext: UserContext; userInputCode?: string }
          • deviceId: string
          • tenantId: string
          • userContext: UserContext
          • Optional userInputCode?: string

        Returns Promise<{ codeId: string; codeLifetime: number; deviceId: string; linkCode: string; preAuthSessionId: string; status: "OK"; timeCreated: number; userInputCode: string } | { status: "RESTART_FLOW_ERROR" | "USER_INPUT_CODE_ALREADY_USED_ERROR" }>

    • listCodesByDeviceId:function
      • listCodesByDeviceId(input: { deviceId: string; tenantId: string; userContext: UserContext }): Promise<undefined | DeviceType>
      • Parameters

        • input: { deviceId: string; tenantId: string; userContext: UserContext }
          • deviceId: string
          • tenantId: string
          • userContext: UserContext

        Returns Promise<undefined | DeviceType>

    • listCodesByEmail:function
      • listCodesByEmail(input: { email: string; tenantId: string; userContext: UserContext }): Promise<DeviceType[]>
      • Parameters

        • input: { email: string; tenantId: string; userContext: UserContext }
          • email: string
          • tenantId: string
          • userContext: UserContext

        Returns Promise<DeviceType[]>

    • listCodesByPhoneNumber:function
      • listCodesByPhoneNumber(input: { phoneNumber: string; tenantId: string; userContext: UserContext }): Promise<DeviceType[]>
      • Parameters

        • input: { phoneNumber: string; tenantId: string; userContext: UserContext }
          • phoneNumber: string
          • tenantId: string
          • userContext: UserContext

        Returns Promise<DeviceType[]>

    • listCodesByPreAuthSessionId:function
      • listCodesByPreAuthSessionId(input: { preAuthSessionId: string; tenantId: string; userContext: UserContext }): Promise<undefined | DeviceType>
      • Parameters

        • input: { preAuthSessionId: string; tenantId: string; userContext: UserContext }
          • preAuthSessionId: string
          • tenantId: string
          • userContext: UserContext

        Returns Promise<undefined | DeviceType>

    • revokeAllCodes:function
      • revokeAllCodes(input: { email: string; tenantId: string; userContext: UserContext } | { phoneNumber: string; tenantId: string; userContext: UserContext }): Promise<{ status: "OK" }>
      • Parameters

        • input: { email: string; tenantId: string; userContext: UserContext } | { phoneNumber: string; tenantId: string; userContext: UserContext }

        Returns Promise<{ status: "OK" }>

    • revokeCode:function
      • revokeCode(input: { codeId: string; tenantId: string; userContext: UserContext } | { preAuthSessionId: string; tenantId: string; userContext: UserContext }): Promise<{ status: "OK" }>
      • Parameters

        • input: { codeId: string; tenantId: string; userContext: UserContext } | { preAuthSessionId: string; tenantId: string; userContext: UserContext }

        Returns Promise<{ status: "OK" }>

    • updateUser:function
      • updateUser(input: { email?: string | null; phoneNumber?: string | null; recipeUserId: RecipeUserId; userContext: UserContext }): Promise<{ status: "OK" | "UNKNOWN_USER_ID_ERROR" | "EMAIL_ALREADY_EXISTS_ERROR" | "PHONE_NUMBER_ALREADY_EXISTS_ERROR" } | { reason: string; status: "EMAIL_CHANGE_NOT_ALLOWED_ERROR" | "PHONE_NUMBER_CHANGE_NOT_ALLOWED_ERROR" }>
      • Parameters

        • input: { email?: string | null; phoneNumber?: string | null; recipeUserId: RecipeUserId; userContext: UserContext }
          • Optional email?: string | null
          • Optional phoneNumber?: string | null
          • recipeUserId: RecipeUserId
          • userContext: UserContext

        Returns Promise<{ status: "OK" | "UNKNOWN_USER_ID_ERROR" | "EMAIL_ALREADY_EXISTS_ERROR" | "PHONE_NUMBER_ALREADY_EXISTS_ERROR" } | { reason: string; status: "EMAIL_CHANGE_NOT_ALLOWED_ERROR" | "PHONE_NUMBER_CHANGE_NOT_ALLOWED_ERROR" }>

    Variables

    Error: typeof default = Wrapper.Error

    Functions

    • checkCode(input: { deviceId: string; preAuthSessionId: string; tenantId: string; userContext?: Record<string, any>; userInputCode: string } | { linkCode: string; preAuthSessionId: string; tenantId: string; userContext?: Record<string, any> }): Promise<{ consumedDevice: { email?: string; failedCodeInputAttemptCount: number; phoneNumber?: string; preAuthSessionId: string }; status: "OK" } | { failedCodeInputAttemptCount: number; maximumCodeInputAttempts: number; status: "INCORRECT_USER_INPUT_CODE_ERROR" | "EXPIRED_USER_INPUT_CODE_ERROR" } | { status: "RESTART_FLOW_ERROR" }>
    • Parameters

      • input: { deviceId: string; preAuthSessionId: string; tenantId: string; userContext?: Record<string, any>; userInputCode: string } | { linkCode: string; preAuthSessionId: string; tenantId: string; userContext?: Record<string, any> }

      Returns Promise<{ consumedDevice: { email?: string; failedCodeInputAttemptCount: number; phoneNumber?: string; preAuthSessionId: string }; status: "OK" } | { failedCodeInputAttemptCount: number; maximumCodeInputAttempts: number; status: "INCORRECT_USER_INPUT_CODE_ERROR" | "EXPIRED_USER_INPUT_CODE_ERROR" } | { status: "RESTART_FLOW_ERROR" }>

    • consumeCode(input: { deviceId: string; preAuthSessionId: string; session?: undefined; tenantId: string; userContext?: Record<string, any>; userInputCode: string } | { linkCode: string; preAuthSessionId: string; session?: undefined; tenantId: string; userContext?: Record<string, any> }): Promise<{ consumedDevice: { email?: string; failedCodeInputAttemptCount: number; phoneNumber?: string; preAuthSessionId: string }; createdNewRecipeUser: boolean; recipeUserId: RecipeUserId; status: "OK"; user: User } | { failedCodeInputAttemptCount: number; maximumCodeInputAttempts: number; status: "INCORRECT_USER_INPUT_CODE_ERROR" | "EXPIRED_USER_INPUT_CODE_ERROR" } | { status: "RESTART_FLOW_ERROR" }>
    • consumeCode(input: { deviceId: string; preAuthSessionId: string; session: SessionContainer; tenantId: string; userContext?: Record<string, any>; userInputCode: string } | { linkCode: string; preAuthSessionId: string; session: SessionContainer; tenantId: string; userContext?: Record<string, any> }): Promise<{ consumedDevice: { email?: string; failedCodeInputAttemptCount: number; phoneNumber?: string; preAuthSessionId: string }; createdNewRecipeUser: boolean; recipeUserId: RecipeUserId; status: "OK"; user: User } | { failedCodeInputAttemptCount: number; maximumCodeInputAttempts: number; status: "INCORRECT_USER_INPUT_CODE_ERROR" | "EXPIRED_USER_INPUT_CODE_ERROR" } | { status: "RESTART_FLOW_ERROR" } | { reason: "ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR" | "RECIPE_USER_ID_ALREADY_LINKED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR" | "EMAIL_VERIFICATION_REQUIRED" | "SESSION_USER_ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR"; status: "LINKING_TO_SESSION_USER_FAILED" }>
    • Parameters

      • input: { deviceId: string; preAuthSessionId: string; session?: undefined; tenantId: string; userContext?: Record<string, any>; userInputCode: string } | { linkCode: string; preAuthSessionId: string; session?: undefined; tenantId: string; userContext?: Record<string, any> }

      Returns Promise<{ consumedDevice: { email?: string; failedCodeInputAttemptCount: number; phoneNumber?: string; preAuthSessionId: string }; createdNewRecipeUser: boolean; recipeUserId: RecipeUserId; status: "OK"; user: User } | { failedCodeInputAttemptCount: number; maximumCodeInputAttempts: number; status: "INCORRECT_USER_INPUT_CODE_ERROR" | "EXPIRED_USER_INPUT_CODE_ERROR" } | { status: "RESTART_FLOW_ERROR" }>

    • Parameters

      • input: { deviceId: string; preAuthSessionId: string; session: SessionContainer; tenantId: string; userContext?: Record<string, any>; userInputCode: string } | { linkCode: string; preAuthSessionId: string; session: SessionContainer; tenantId: string; userContext?: Record<string, any> }

      Returns Promise<{ consumedDevice: { email?: string; failedCodeInputAttemptCount: number; phoneNumber?: string; preAuthSessionId: string }; createdNewRecipeUser: boolean; recipeUserId: RecipeUserId; status: "OK"; user: User } | { failedCodeInputAttemptCount: number; maximumCodeInputAttempts: number; status: "INCORRECT_USER_INPUT_CODE_ERROR" | "EXPIRED_USER_INPUT_CODE_ERROR" } | { status: "RESTART_FLOW_ERROR" } | { reason: "ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR" | "RECIPE_USER_ID_ALREADY_LINKED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR" | "EMAIL_VERIFICATION_REQUIRED" | "SESSION_USER_ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR"; status: "LINKING_TO_SESSION_USER_FAILED" }>

    • createCode(input: { email: string } & { session?: SessionContainer; tenantId: string; userContext?: Record<string, any>; userInputCode?: string } & { phoneNumber: string } & { session?: SessionContainer; tenantId: string; userContext?: Record<string, any>; userInputCode?: string }): Promise<{ codeId: string; codeLifetime: number; deviceId: string; linkCode: string; preAuthSessionId: string; status: "OK"; timeCreated: number; userInputCode: string }>
    • Parameters

      • input: { email: string } & { session?: SessionContainer; tenantId: string; userContext?: Record<string, any>; userInputCode?: string } & { phoneNumber: string } & { session?: SessionContainer; tenantId: string; userContext?: Record<string, any>; userInputCode?: string }

      Returns Promise<{ codeId: string; codeLifetime: number; deviceId: string; linkCode: string; preAuthSessionId: string; status: "OK"; timeCreated: number; userInputCode: string }>

    • createMagicLink(input: { email: string; tenantId: string; userContext?: Record<string, any> } | { phoneNumber: string; tenantId: string; userContext?: Record<string, any> }): Promise<string>
    • Parameters

      • input: { email: string; tenantId: string; userContext?: Record<string, any> } | { phoneNumber: string; tenantId: string; userContext?: Record<string, any> }

      Returns Promise<string>

    • createNewCodeForDevice(input: { deviceId: string; tenantId: string; userContext?: Record<string, any>; userInputCode?: string }): Promise<{ codeId: string; codeLifetime: number; deviceId: string; linkCode: string; preAuthSessionId: string; status: "OK"; timeCreated: number; userInputCode: string } | { status: "RESTART_FLOW_ERROR" | "USER_INPUT_CODE_ALREADY_USED_ERROR" }>
    • Parameters

      • input: { deviceId: string; tenantId: string; userContext?: Record<string, any>; userInputCode?: string }
        • deviceId: string
        • tenantId: string
        • Optional userContext?: Record<string, any>
        • Optional userInputCode?: string

      Returns Promise<{ codeId: string; codeLifetime: number; deviceId: string; linkCode: string; preAuthSessionId: string; status: "OK"; timeCreated: number; userInputCode: string } | { status: "RESTART_FLOW_ERROR" | "USER_INPUT_CODE_ALREADY_USED_ERROR" }>

    • init(config: TypeInput): RecipeListFunction
    • listCodesByDeviceId(input: { deviceId: string; tenantId: string; userContext?: Record<string, any> }): Promise<undefined | DeviceType>
    • Parameters

      • input: { deviceId: string; tenantId: string; userContext?: Record<string, any> }
        • deviceId: string
        • tenantId: string
        • Optional userContext?: Record<string, any>

      Returns Promise<undefined | DeviceType>

    • listCodesByEmail(input: { email: string; tenantId: string; userContext?: Record<string, any> }): Promise<DeviceType[]>
    • Parameters

      • input: { email: string; tenantId: string; userContext?: Record<string, any> }
        • email: string
        • tenantId: string
        • Optional userContext?: Record<string, any>

      Returns Promise<DeviceType[]>

    • listCodesByPhoneNumber(input: { phoneNumber: string; tenantId: string; userContext?: Record<string, any> }): Promise<DeviceType[]>
    • Parameters

      • input: { phoneNumber: string; tenantId: string; userContext?: Record<string, any> }
        • phoneNumber: string
        • tenantId: string
        • Optional userContext?: Record<string, any>

      Returns Promise<DeviceType[]>

    • listCodesByPreAuthSessionId(input: { preAuthSessionId: string; tenantId: string; userContext?: Record<string, any> }): Promise<undefined | DeviceType>
    • Parameters

      • input: { preAuthSessionId: string; tenantId: string; userContext?: Record<string, any> }
        • preAuthSessionId: string
        • tenantId: string
        • Optional userContext?: Record<string, any>

      Returns Promise<undefined | DeviceType>

    • revokeAllCodes(input: { email: string; tenantId: string; userContext?: Record<string, any> } | { phoneNumber: string; tenantId: string; userContext?: Record<string, any> }): Promise<{ status: "OK" }>
    • Parameters

      • input: { email: string; tenantId: string; userContext?: Record<string, any> } | { phoneNumber: string; tenantId: string; userContext?: Record<string, any> }

      Returns Promise<{ status: "OK" }>

    • revokeCode(input: { codeId: string; tenantId: string; userContext?: Record<string, any> } | { preAuthSessionId: string; tenantId: string; userContext?: Record<string, any> }): Promise<{ status: "OK" }>
    • Parameters

      • input: { codeId: string; tenantId: string; userContext?: Record<string, any> } | { preAuthSessionId: string; tenantId: string; userContext?: Record<string, any> }

      Returns Promise<{ status: "OK" }>

    • sendEmail(input: TypePasswordlessEmailDeliveryInput & { userContext?: Record<string, any> }): Promise<void>
    • sendSms(input: TypePasswordlessSmsDeliveryInput & { userContext?: Record<string, any> }): Promise<void>
    • signInUp(input: { email: string; session?: SessionContainer; tenantId: string; userContext?: Record<string, any> } | { phoneNumber: string; session?: SessionContainer; tenantId: string; userContext?: Record<string, any> }): Promise<{ createdNewRecipeUser: boolean; recipeUserId: RecipeUserId; status: string; user: User }>
    • updateUser(input: { email?: null | string; phoneNumber?: null | string; recipeUserId: RecipeUserId; userContext?: Record<string, any> }): Promise<{ status: "OK" | "EMAIL_ALREADY_EXISTS_ERROR" | "UNKNOWN_USER_ID_ERROR" | "PHONE_NUMBER_ALREADY_EXISTS_ERROR" } | { reason: string; status: "EMAIL_CHANGE_NOT_ALLOWED_ERROR" | "PHONE_NUMBER_CHANGE_NOT_ALLOWED_ERROR" }>
    • Parameters

      • input: { email?: null | string; phoneNumber?: null | string; recipeUserId: RecipeUserId; userContext?: Record<string, any> }
        • Optional email?: null | string
        • Optional phoneNumber?: null | string
        • recipeUserId: RecipeUserId
        • Optional userContext?: Record<string, any>

      Returns Promise<{ status: "OK" | "EMAIL_ALREADY_EXISTS_ERROR" | "UNKNOWN_USER_ID_ERROR" | "PHONE_NUMBER_ALREADY_EXISTS_ERROR" } | { reason: string; status: "EMAIL_CHANGE_NOT_ALLOWED_ERROR" | "PHONE_NUMBER_CHANGE_NOT_ALLOWED_ERROR" }>

    Legend

    • Variable
    • Function
    • Function with type parameter
    • Type alias
    • Type alias with type parameter
    • Class
    • Class with type parameter
    • Interface

    Settings

    Theme

    Generated using TypeDoc

    \ No newline at end of file +recipe/passwordless | supertokens-node
    Options
    All
    • Public
    • Public/Protected
    • All
    Menu

    Module recipe/passwordless

    Index

    Type Aliases

    APIInterface: { consumeCodePOST?: any; createCodePOST?: any; emailExistsGET?: any; phoneNumberExistsGET?: any; resendCodePOST?: any }

    Type declaration

    • consumeCodePOST?:function
      • consumeCodePOST(input: { deviceId: string; preAuthSessionId: string; userInputCode: string } & { options: APIOptions; session: SessionContainer | undefined; tenantId: string; userContext: UserContext } & { linkCode: string; preAuthSessionId: string } & { options: APIOptions; session: SessionContainer | undefined; tenantId: string; userContext: UserContext }): Promise<GeneralErrorResponse | { createdNewRecipeUser: boolean; session: SessionContainer; status: "OK"; user: User } | { failedCodeInputAttemptCount: number; maximumCodeInputAttempts: number; status: "INCORRECT_USER_INPUT_CODE_ERROR" | "EXPIRED_USER_INPUT_CODE_ERROR" } | { status: "RESTART_FLOW_ERROR" } | { reason: string; status: "SIGN_IN_UP_NOT_ALLOWED" }>
      • Parameters

        • input: { deviceId: string; preAuthSessionId: string; userInputCode: string } & { options: APIOptions; session: SessionContainer | undefined; tenantId: string; userContext: UserContext } & { linkCode: string; preAuthSessionId: string } & { options: APIOptions; session: SessionContainer | undefined; tenantId: string; userContext: UserContext }

        Returns Promise<GeneralErrorResponse | { createdNewRecipeUser: boolean; session: SessionContainer; status: "OK"; user: User } | { failedCodeInputAttemptCount: number; maximumCodeInputAttempts: number; status: "INCORRECT_USER_INPUT_CODE_ERROR" | "EXPIRED_USER_INPUT_CODE_ERROR" } | { status: "RESTART_FLOW_ERROR" } | { reason: string; status: "SIGN_IN_UP_NOT_ALLOWED" }>

    • createCodePOST?:function
      • createCodePOST(input: { email: string } & { options: APIOptions; session: SessionContainer | undefined; tenantId: string; userContext: UserContext } & { phoneNumber: string } & { options: APIOptions; session: SessionContainer | undefined; tenantId: string; userContext: UserContext }): Promise<GeneralErrorResponse | { deviceId: string; flowType: "USER_INPUT_CODE" | "MAGIC_LINK" | "USER_INPUT_CODE_AND_MAGIC_LINK"; preAuthSessionId: string; status: "OK" } | { reason: string; status: "SIGN_IN_UP_NOT_ALLOWED" }>
      • Parameters

        • input: { email: string } & { options: APIOptions; session: SessionContainer | undefined; tenantId: string; userContext: UserContext } & { phoneNumber: string } & { options: APIOptions; session: SessionContainer | undefined; tenantId: string; userContext: UserContext }

        Returns Promise<GeneralErrorResponse | { deviceId: string; flowType: "USER_INPUT_CODE" | "MAGIC_LINK" | "USER_INPUT_CODE_AND_MAGIC_LINK"; preAuthSessionId: string; status: "OK" } | { reason: string; status: "SIGN_IN_UP_NOT_ALLOWED" }>

    • emailExistsGET?:function
      • emailExistsGET(input: { email: string; options: APIOptions; tenantId: string; userContext: UserContext }): Promise<GeneralErrorResponse | { exists: boolean; status: "OK" }>
    • phoneNumberExistsGET?:function
      • phoneNumberExistsGET(input: { options: APIOptions; phoneNumber: string; tenantId: string; userContext: UserContext }): Promise<GeneralErrorResponse | { exists: boolean; status: "OK" }>
      • Parameters

        • input: { options: APIOptions; phoneNumber: string; tenantId: string; userContext: UserContext }
          • options: APIOptions
          • phoneNumber: string
          • tenantId: string
          • userContext: UserContext

        Returns Promise<GeneralErrorResponse | { exists: boolean; status: "OK" }>

    • resendCodePOST?:function
      • resendCodePOST(input: { deviceId: string; preAuthSessionId: string } & { options: APIOptions; session: SessionContainer | undefined; tenantId: string; userContext: UserContext }): Promise<GeneralErrorResponse | { status: "RESTART_FLOW_ERROR" | "OK" }>
    APIOptions: { appInfo: NormalisedAppinfo; config: TypeNormalisedInput; emailDelivery: default<TypePasswordlessEmailDeliveryInput>; isInServerlessEnv: boolean; recipeId: string; recipeImplementation: RecipeInterface; req: BaseRequest; res: BaseResponse; smsDelivery: default<TypePasswordlessSmsDeliveryInput> }

    Type declaration

    • appInfo: NormalisedAppinfo
    • config: TypeNormalisedInput
    • emailDelivery: default<TypePasswordlessEmailDeliveryInput>
    • isInServerlessEnv: boolean
    • recipeId: string
    • recipeImplementation: RecipeInterface
    • req: BaseRequest
    • res: BaseResponse
    • smsDelivery: default<TypePasswordlessSmsDeliveryInput>
    RecipeInterface: { checkCode: any; consumeCode: any; createCode: any; createNewCodeForDevice: any; listCodesByDeviceId: any; listCodesByEmail: any; listCodesByPhoneNumber: any; listCodesByPreAuthSessionId: any; revokeAllCodes: any; revokeCode: any; updateUser: any }

    Type declaration

    • checkCode:function
      • checkCode(input: { deviceId: string; preAuthSessionId: string; tenantId: string; userContext: UserContext; userInputCode: string } | { linkCode: string; preAuthSessionId: string; tenantId: string; userContext: UserContext }): Promise<{ consumedDevice: { email?: string; failedCodeInputAttemptCount: number; phoneNumber?: string; preAuthSessionId: string }; status: "OK" } | { failedCodeInputAttemptCount: number; maximumCodeInputAttempts: number; status: "INCORRECT_USER_INPUT_CODE_ERROR" | "EXPIRED_USER_INPUT_CODE_ERROR" } | { status: "RESTART_FLOW_ERROR" }>
      • Parameters

        • input: { deviceId: string; preAuthSessionId: string; tenantId: string; userContext: UserContext; userInputCode: string } | { linkCode: string; preAuthSessionId: string; tenantId: string; userContext: UserContext }

        Returns Promise<{ consumedDevice: { email?: string; failedCodeInputAttemptCount: number; phoneNumber?: string; preAuthSessionId: string }; status: "OK" } | { failedCodeInputAttemptCount: number; maximumCodeInputAttempts: number; status: "INCORRECT_USER_INPUT_CODE_ERROR" | "EXPIRED_USER_INPUT_CODE_ERROR" } | { status: "RESTART_FLOW_ERROR" }>

    • consumeCode:function
      • consumeCode(input: { deviceId: string; preAuthSessionId: string; session: SessionContainer | undefined; tenantId: string; userContext: UserContext; userInputCode: string } | { linkCode: string; preAuthSessionId: string; session: SessionContainer | undefined; tenantId: string; userContext: UserContext }): Promise<{ consumedDevice: { email?: string; failedCodeInputAttemptCount: number; phoneNumber?: string; preAuthSessionId: string }; createdNewRecipeUser: boolean; recipeUserId: RecipeUserId; status: "OK"; user: User } | { failedCodeInputAttemptCount: number; maximumCodeInputAttempts: number; status: "INCORRECT_USER_INPUT_CODE_ERROR" | "EXPIRED_USER_INPUT_CODE_ERROR" } | { status: "RESTART_FLOW_ERROR" } | { reason: "EMAIL_VERIFICATION_REQUIRED" | "RECIPE_USER_ID_ALREADY_LINKED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR" | "ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR" | "SESSION_USER_ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR"; status: "LINKING_TO_SESSION_USER_FAILED" }>
      • Parameters

        • input: { deviceId: string; preAuthSessionId: string; session: SessionContainer | undefined; tenantId: string; userContext: UserContext; userInputCode: string } | { linkCode: string; preAuthSessionId: string; session: SessionContainer | undefined; tenantId: string; userContext: UserContext }

        Returns Promise<{ consumedDevice: { email?: string; failedCodeInputAttemptCount: number; phoneNumber?: string; preAuthSessionId: string }; createdNewRecipeUser: boolean; recipeUserId: RecipeUserId; status: "OK"; user: User } | { failedCodeInputAttemptCount: number; maximumCodeInputAttempts: number; status: "INCORRECT_USER_INPUT_CODE_ERROR" | "EXPIRED_USER_INPUT_CODE_ERROR" } | { status: "RESTART_FLOW_ERROR" } | { reason: "EMAIL_VERIFICATION_REQUIRED" | "RECIPE_USER_ID_ALREADY_LINKED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR" | "ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR" | "SESSION_USER_ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR"; status: "LINKING_TO_SESSION_USER_FAILED" }>

    • createCode:function
      • createCode(input: { email: string } & { session: SessionContainer | undefined; tenantId: string; userContext: UserContext; userInputCode?: string } & { phoneNumber: string } & { session: SessionContainer | undefined; tenantId: string; userContext: UserContext; userInputCode?: string }): Promise<{ codeId: string; codeLifetime: number; deviceId: string; linkCode: string; preAuthSessionId: string; status: "OK"; timeCreated: number; userInputCode: string }>
      • Parameters

        • input: { email: string } & { session: SessionContainer | undefined; tenantId: string; userContext: UserContext; userInputCode?: string } & { phoneNumber: string } & { session: SessionContainer | undefined; tenantId: string; userContext: UserContext; userInputCode?: string }

        Returns Promise<{ codeId: string; codeLifetime: number; deviceId: string; linkCode: string; preAuthSessionId: string; status: "OK"; timeCreated: number; userInputCode: string }>

    • createNewCodeForDevice:function
      • createNewCodeForDevice(input: { deviceId: string; tenantId: string; userContext: UserContext; userInputCode?: string }): Promise<{ codeId: string; codeLifetime: number; deviceId: string; linkCode: string; preAuthSessionId: string; status: "OK"; timeCreated: number; userInputCode: string } | { status: "RESTART_FLOW_ERROR" | "USER_INPUT_CODE_ALREADY_USED_ERROR" }>
      • Parameters

        • input: { deviceId: string; tenantId: string; userContext: UserContext; userInputCode?: string }
          • deviceId: string
          • tenantId: string
          • userContext: UserContext
          • Optional userInputCode?: string

        Returns Promise<{ codeId: string; codeLifetime: number; deviceId: string; linkCode: string; preAuthSessionId: string; status: "OK"; timeCreated: number; userInputCode: string } | { status: "RESTART_FLOW_ERROR" | "USER_INPUT_CODE_ALREADY_USED_ERROR" }>

    • listCodesByDeviceId:function
      • listCodesByDeviceId(input: { deviceId: string; tenantId: string; userContext: UserContext }): Promise<undefined | DeviceType>
      • Parameters

        • input: { deviceId: string; tenantId: string; userContext: UserContext }
          • deviceId: string
          • tenantId: string
          • userContext: UserContext

        Returns Promise<undefined | DeviceType>

    • listCodesByEmail:function
      • listCodesByEmail(input: { email: string; tenantId: string; userContext: UserContext }): Promise<DeviceType[]>
      • Parameters

        • input: { email: string; tenantId: string; userContext: UserContext }
          • email: string
          • tenantId: string
          • userContext: UserContext

        Returns Promise<DeviceType[]>

    • listCodesByPhoneNumber:function
      • listCodesByPhoneNumber(input: { phoneNumber: string; tenantId: string; userContext: UserContext }): Promise<DeviceType[]>
      • Parameters

        • input: { phoneNumber: string; tenantId: string; userContext: UserContext }
          • phoneNumber: string
          • tenantId: string
          • userContext: UserContext

        Returns Promise<DeviceType[]>

    • listCodesByPreAuthSessionId:function
      • listCodesByPreAuthSessionId(input: { preAuthSessionId: string; tenantId: string; userContext: UserContext }): Promise<undefined | DeviceType>
      • Parameters

        • input: { preAuthSessionId: string; tenantId: string; userContext: UserContext }
          • preAuthSessionId: string
          • tenantId: string
          • userContext: UserContext

        Returns Promise<undefined | DeviceType>

    • revokeAllCodes:function
      • revokeAllCodes(input: { email: string; tenantId: string; userContext: UserContext } | { phoneNumber: string; tenantId: string; userContext: UserContext }): Promise<{ status: "OK" }>
      • Parameters

        • input: { email: string; tenantId: string; userContext: UserContext } | { phoneNumber: string; tenantId: string; userContext: UserContext }

        Returns Promise<{ status: "OK" }>

    • revokeCode:function
      • revokeCode(input: { codeId: string; tenantId: string; userContext: UserContext } | { preAuthSessionId: string; tenantId: string; userContext: UserContext }): Promise<{ status: "OK" }>
      • Parameters

        • input: { codeId: string; tenantId: string; userContext: UserContext } | { preAuthSessionId: string; tenantId: string; userContext: UserContext }

        Returns Promise<{ status: "OK" }>

    • updateUser:function
      • updateUser(input: { email?: string | null; phoneNumber?: string | null; recipeUserId: RecipeUserId; userContext: UserContext }): Promise<{ status: "OK" | "UNKNOWN_USER_ID_ERROR" | "EMAIL_ALREADY_EXISTS_ERROR" | "PHONE_NUMBER_ALREADY_EXISTS_ERROR" } | { reason: string; status: "EMAIL_CHANGE_NOT_ALLOWED_ERROR" | "PHONE_NUMBER_CHANGE_NOT_ALLOWED_ERROR" }>
      • Parameters

        • input: { email?: string | null; phoneNumber?: string | null; recipeUserId: RecipeUserId; userContext: UserContext }
          • Optional email?: string | null
          • Optional phoneNumber?: string | null
          • recipeUserId: RecipeUserId
          • userContext: UserContext

        Returns Promise<{ status: "OK" | "UNKNOWN_USER_ID_ERROR" | "EMAIL_ALREADY_EXISTS_ERROR" | "PHONE_NUMBER_ALREADY_EXISTS_ERROR" } | { reason: string; status: "EMAIL_CHANGE_NOT_ALLOWED_ERROR" | "PHONE_NUMBER_CHANGE_NOT_ALLOWED_ERROR" }>

    Variables

    Error: typeof default = Wrapper.Error

    Functions

    • checkCode(input: { deviceId: string; preAuthSessionId: string; tenantId: string; userContext?: Record<string, any>; userInputCode: string } | { linkCode: string; preAuthSessionId: string; tenantId: string; userContext?: Record<string, any> }): Promise<{ consumedDevice: { email?: string; failedCodeInputAttemptCount: number; phoneNumber?: string; preAuthSessionId: string }; status: "OK" } | { failedCodeInputAttemptCount: number; maximumCodeInputAttempts: number; status: "INCORRECT_USER_INPUT_CODE_ERROR" | "EXPIRED_USER_INPUT_CODE_ERROR" } | { status: "RESTART_FLOW_ERROR" }>
    • Parameters

      • input: { deviceId: string; preAuthSessionId: string; tenantId: string; userContext?: Record<string, any>; userInputCode: string } | { linkCode: string; preAuthSessionId: string; tenantId: string; userContext?: Record<string, any> }

      Returns Promise<{ consumedDevice: { email?: string; failedCodeInputAttemptCount: number; phoneNumber?: string; preAuthSessionId: string }; status: "OK" } | { failedCodeInputAttemptCount: number; maximumCodeInputAttempts: number; status: "INCORRECT_USER_INPUT_CODE_ERROR" | "EXPIRED_USER_INPUT_CODE_ERROR" } | { status: "RESTART_FLOW_ERROR" }>

    • consumeCode(input: { deviceId: string; preAuthSessionId: string; session?: undefined; tenantId: string; userContext?: Record<string, any>; userInputCode: string } | { linkCode: string; preAuthSessionId: string; session?: undefined; tenantId: string; userContext?: Record<string, any> }): Promise<{ consumedDevice: { email?: string; failedCodeInputAttemptCount: number; phoneNumber?: string; preAuthSessionId: string }; createdNewRecipeUser: boolean; recipeUserId: RecipeUserId; status: "OK"; user: User } | { failedCodeInputAttemptCount: number; maximumCodeInputAttempts: number; status: "INCORRECT_USER_INPUT_CODE_ERROR" | "EXPIRED_USER_INPUT_CODE_ERROR" } | { status: "RESTART_FLOW_ERROR" }>
    • consumeCode(input: { deviceId: string; preAuthSessionId: string; session: SessionContainer; tenantId: string; userContext?: Record<string, any>; userInputCode: string } | { linkCode: string; preAuthSessionId: string; session: SessionContainer; tenantId: string; userContext?: Record<string, any> }): Promise<{ consumedDevice: { email?: string; failedCodeInputAttemptCount: number; phoneNumber?: string; preAuthSessionId: string }; createdNewRecipeUser: boolean; recipeUserId: RecipeUserId; status: "OK"; user: User } | { failedCodeInputAttemptCount: number; maximumCodeInputAttempts: number; status: "INCORRECT_USER_INPUT_CODE_ERROR" | "EXPIRED_USER_INPUT_CODE_ERROR" } | { status: "RESTART_FLOW_ERROR" } | { reason: "ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR" | "RECIPE_USER_ID_ALREADY_LINKED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR" | "EMAIL_VERIFICATION_REQUIRED" | "SESSION_USER_ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR"; status: "LINKING_TO_SESSION_USER_FAILED" }>
    • Parameters

      • input: { deviceId: string; preAuthSessionId: string; session?: undefined; tenantId: string; userContext?: Record<string, any>; userInputCode: string } | { linkCode: string; preAuthSessionId: string; session?: undefined; tenantId: string; userContext?: Record<string, any> }

      Returns Promise<{ consumedDevice: { email?: string; failedCodeInputAttemptCount: number; phoneNumber?: string; preAuthSessionId: string }; createdNewRecipeUser: boolean; recipeUserId: RecipeUserId; status: "OK"; user: User } | { failedCodeInputAttemptCount: number; maximumCodeInputAttempts: number; status: "INCORRECT_USER_INPUT_CODE_ERROR" | "EXPIRED_USER_INPUT_CODE_ERROR" } | { status: "RESTART_FLOW_ERROR" }>

    • Parameters

      • input: { deviceId: string; preAuthSessionId: string; session: SessionContainer; tenantId: string; userContext?: Record<string, any>; userInputCode: string } | { linkCode: string; preAuthSessionId: string; session: SessionContainer; tenantId: string; userContext?: Record<string, any> }

      Returns Promise<{ consumedDevice: { email?: string; failedCodeInputAttemptCount: number; phoneNumber?: string; preAuthSessionId: string }; createdNewRecipeUser: boolean; recipeUserId: RecipeUserId; status: "OK"; user: User } | { failedCodeInputAttemptCount: number; maximumCodeInputAttempts: number; status: "INCORRECT_USER_INPUT_CODE_ERROR" | "EXPIRED_USER_INPUT_CODE_ERROR" } | { status: "RESTART_FLOW_ERROR" } | { reason: "ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR" | "RECIPE_USER_ID_ALREADY_LINKED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR" | "EMAIL_VERIFICATION_REQUIRED" | "SESSION_USER_ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR"; status: "LINKING_TO_SESSION_USER_FAILED" }>

    • createCode(input: { email: string } & { session?: SessionContainer; tenantId: string; userContext?: Record<string, any>; userInputCode?: string } & { phoneNumber: string } & { session?: SessionContainer; tenantId: string; userContext?: Record<string, any>; userInputCode?: string }): Promise<{ codeId: string; codeLifetime: number; deviceId: string; linkCode: string; preAuthSessionId: string; status: "OK"; timeCreated: number; userInputCode: string }>
    • Parameters

      • input: { email: string } & { session?: SessionContainer; tenantId: string; userContext?: Record<string, any>; userInputCode?: string } & { phoneNumber: string } & { session?: SessionContainer; tenantId: string; userContext?: Record<string, any>; userInputCode?: string }

      Returns Promise<{ codeId: string; codeLifetime: number; deviceId: string; linkCode: string; preAuthSessionId: string; status: "OK"; timeCreated: number; userInputCode: string }>

    • createMagicLink(input: { email: string; tenantId: string; userContext?: Record<string, any> } | { phoneNumber: string; tenantId: string; userContext?: Record<string, any> }): Promise<string>
    • Parameters

      • input: { email: string; tenantId: string; userContext?: Record<string, any> } | { phoneNumber: string; tenantId: string; userContext?: Record<string, any> }

      Returns Promise<string>

    • createNewCodeForDevice(input: { deviceId: string; tenantId: string; userContext?: Record<string, any>; userInputCode?: string }): Promise<{ codeId: string; codeLifetime: number; deviceId: string; linkCode: string; preAuthSessionId: string; status: "OK"; timeCreated: number; userInputCode: string } | { status: "RESTART_FLOW_ERROR" | "USER_INPUT_CODE_ALREADY_USED_ERROR" }>
    • Parameters

      • input: { deviceId: string; tenantId: string; userContext?: Record<string, any>; userInputCode?: string }
        • deviceId: string
        • tenantId: string
        • Optional userContext?: Record<string, any>
        • Optional userInputCode?: string

      Returns Promise<{ codeId: string; codeLifetime: number; deviceId: string; linkCode: string; preAuthSessionId: string; status: "OK"; timeCreated: number; userInputCode: string } | { status: "RESTART_FLOW_ERROR" | "USER_INPUT_CODE_ALREADY_USED_ERROR" }>

    • init(config: TypeInput): RecipeListFunction
    • listCodesByDeviceId(input: { deviceId: string; tenantId: string; userContext?: Record<string, any> }): Promise<undefined | DeviceType>
    • Parameters

      • input: { deviceId: string; tenantId: string; userContext?: Record<string, any> }
        • deviceId: string
        • tenantId: string
        • Optional userContext?: Record<string, any>

      Returns Promise<undefined | DeviceType>

    • listCodesByEmail(input: { email: string; tenantId: string; userContext?: Record<string, any> }): Promise<DeviceType[]>
    • Parameters

      • input: { email: string; tenantId: string; userContext?: Record<string, any> }
        • email: string
        • tenantId: string
        • Optional userContext?: Record<string, any>

      Returns Promise<DeviceType[]>

    • listCodesByPhoneNumber(input: { phoneNumber: string; tenantId: string; userContext?: Record<string, any> }): Promise<DeviceType[]>
    • Parameters

      • input: { phoneNumber: string; tenantId: string; userContext?: Record<string, any> }
        • phoneNumber: string
        • tenantId: string
        • Optional userContext?: Record<string, any>

      Returns Promise<DeviceType[]>

    • listCodesByPreAuthSessionId(input: { preAuthSessionId: string; tenantId: string; userContext?: Record<string, any> }): Promise<undefined | DeviceType>
    • Parameters

      • input: { preAuthSessionId: string; tenantId: string; userContext?: Record<string, any> }
        • preAuthSessionId: string
        • tenantId: string
        • Optional userContext?: Record<string, any>

      Returns Promise<undefined | DeviceType>

    • revokeAllCodes(input: { email: string; tenantId: string; userContext?: Record<string, any> } | { phoneNumber: string; tenantId: string; userContext?: Record<string, any> }): Promise<{ status: "OK" }>
    • Parameters

      • input: { email: string; tenantId: string; userContext?: Record<string, any> } | { phoneNumber: string; tenantId: string; userContext?: Record<string, any> }

      Returns Promise<{ status: "OK" }>

    • revokeCode(input: { codeId: string; tenantId: string; userContext?: Record<string, any> } | { preAuthSessionId: string; tenantId: string; userContext?: Record<string, any> }): Promise<{ status: "OK" }>
    • Parameters

      • input: { codeId: string; tenantId: string; userContext?: Record<string, any> } | { preAuthSessionId: string; tenantId: string; userContext?: Record<string, any> }

      Returns Promise<{ status: "OK" }>

    • sendEmail(input: TypePasswordlessEmailDeliveryInput & { userContext?: Record<string, any> }): Promise<void>
    • sendSms(input: TypePasswordlessSmsDeliveryInput & { userContext?: Record<string, any> }): Promise<void>
    • signInUp(input: { email: string; session?: SessionContainer; tenantId: string; userContext?: Record<string, any> } | { phoneNumber: string; session?: SessionContainer; tenantId: string; userContext?: Record<string, any> }): Promise<{ createdNewRecipeUser: boolean; recipeUserId: RecipeUserId; status: string; user: User }>
    • updateUser(input: { email?: null | string; phoneNumber?: null | string; recipeUserId: RecipeUserId; userContext?: Record<string, any> }): Promise<{ status: "OK" | "EMAIL_ALREADY_EXISTS_ERROR" | "UNKNOWN_USER_ID_ERROR" | "PHONE_NUMBER_ALREADY_EXISTS_ERROR" } | { reason: string; status: "EMAIL_CHANGE_NOT_ALLOWED_ERROR" | "PHONE_NUMBER_CHANGE_NOT_ALLOWED_ERROR" }>
    • Parameters

      • input: { email?: null | string; phoneNumber?: null | string; recipeUserId: RecipeUserId; userContext?: Record<string, any> }
        • Optional email?: null | string
        • Optional phoneNumber?: null | string
        • recipeUserId: RecipeUserId
        • Optional userContext?: Record<string, any>

      Returns Promise<{ status: "OK" | "EMAIL_ALREADY_EXISTS_ERROR" | "UNKNOWN_USER_ID_ERROR" | "PHONE_NUMBER_ALREADY_EXISTS_ERROR" } | { reason: string; status: "EMAIL_CHANGE_NOT_ALLOWED_ERROR" | "PHONE_NUMBER_CHANGE_NOT_ALLOWED_ERROR" }>

    Legend

    • Variable
    • Function
    • Function with type parameter
    • Type alias
    • Type alias with type parameter
    • Class
    • Class with type parameter
    • Interface

    Settings

    Theme

    Generated using TypeDoc

    \ No newline at end of file diff --git a/docs/modules/recipe_session.html b/docs/modules/recipe_session.html index 9ffe5d8a8..57ccc0340 100644 --- a/docs/modules/recipe_session.html +++ b/docs/modules/recipe_session.html @@ -1,13 +1,13 @@ -recipe/session | supertokens-node
    Options
    All
    • Public
    • Public/Protected
    • All
    Menu

    Module recipe/session

    Index

    Type Aliases

    APIInterface: { refreshPOST: undefined | ((input: { options: APIOptions; userContext: UserContext }) => Promise<SessionContainer>); signOutPOST: undefined | ((input: { options: APIOptions; session: SessionContainer; userContext: UserContext }) => Promise<{ status: "OK" } | GeneralErrorResponse>); verifySession: any }

    Type declaration

  • mergeIntoAccessTokenPayload:function
    • mergeIntoAccessTokenPayload(input: { accessTokenPayloadUpdate: JSONObject; sessionHandle: string; userContext: UserContext }): Promise<boolean>
  • refreshSession:function
    • refreshSession(input: { antiCsrfToken?: string; disableAntiCsrf: boolean; refreshToken: string; userContext: UserContext }): Promise<SessionContainer>
    • Parameters

      • input: { antiCsrfToken?: string; disableAntiCsrf: boolean; refreshToken: string; userContext: UserContext }
        • Optional antiCsrfToken?: string
        • disableAntiCsrf: boolean
        • refreshToken: string
        • userContext: UserContext

      Returns Promise<SessionContainer>

  • regenerateAccessToken:function
    • regenerateAccessToken(input: { accessToken: string; newAccessTokenPayload?: any; userContext: UserContext }): Promise<undefined | { accessToken?: { createdTime: number; expiry: number; token: string }; session: { handle: string; recipeUserId: RecipeUserId; tenantId: string; userDataInJWT: any; userId: string }; status: "OK" }>
    • Parameters

      • input: { accessToken: string; newAccessTokenPayload?: any; userContext: UserContext }
        • accessToken: string
        • Optional newAccessTokenPayload?: any
        • userContext: UserContext

      Returns Promise<undefined | { accessToken?: { createdTime: number; expiry: number; token: string }; session: { handle: string; recipeUserId: RecipeUserId; tenantId: string; userDataInJWT: any; userId: string }; status: "OK" }>

      Returns false if the sessionHandle does not exist

      +
  • removeClaim:function
    • removeClaim(input: { claim: SessionClaim<any>; sessionHandle: string; userContext: UserContext }): Promise<boolean>
    • Parameters

      • input: { claim: SessionClaim<any>; sessionHandle: string; userContext: UserContext }
        • claim: SessionClaim<any>
        • sessionHandle: string
        • userContext: UserContext

      Returns Promise<boolean>

  • revokeAllSessionsForUser:function
    • revokeAllSessionsForUser(input: { revokeAcrossAllTenants?: boolean; revokeSessionsForLinkedAccounts: boolean; tenantId: string; userContext: UserContext; userId: string }): Promise<string[]>
    • Parameters

      • input: { revokeAcrossAllTenants?: boolean; revokeSessionsForLinkedAccounts: boolean; tenantId: string; userContext: UserContext; userId: string }
        • Optional revokeAcrossAllTenants?: boolean
        • revokeSessionsForLinkedAccounts: boolean
        • tenantId: string
        • userContext: UserContext
        • userId: string

      Returns Promise<string[]>

  • revokeMultipleSessions:function
    • revokeMultipleSessions(input: { sessionHandles: string[]; userContext: UserContext }): Promise<string[]>
    • Parameters

      • input: { sessionHandles: string[]; userContext: UserContext }
        • sessionHandles: string[]
        • userContext: UserContext

      Returns Promise<string[]>

  • revokeSession:function
    • revokeSession(input: { sessionHandle: string; userContext: UserContext }): Promise<boolean>
    • Parameters

      • input: { sessionHandle: string; userContext: UserContext }
        • sessionHandle: string
        • userContext: UserContext

      Returns Promise<boolean>

  • setClaimValue:function
    • setClaimValue<T>(input: { claim: SessionClaim<T>; sessionHandle: string; userContext: UserContext; value: T }): Promise<boolean>
    • Type Parameters

      • T

      Parameters

      • input: { claim: SessionClaim<T>; sessionHandle: string; userContext: UserContext; value: T }
        • claim: SessionClaim<T>
        • sessionHandle: string
        • userContext: UserContext
        • value: T

      Returns Promise<boolean>

  • updateSessionDataInDatabase:function
    • updateSessionDataInDatabase(input: { newSessionData: any; sessionHandle: string; userContext: UserContext }): Promise<boolean>
    • Parameters

      • input: { newSessionData: any; sessionHandle: string; userContext: UserContext }
        • newSessionData: any
        • sessionHandle: string
        • userContext: UserContext

      Returns Promise<boolean>

  • validateClaims:function
    • validateClaims(input: { accessTokenPayload: any; claimValidators: SessionClaimValidator[]; recipeUserId: RecipeUserId; userContext: UserContext; userId: string }): Promise<{ accessTokenPayloadUpdate?: any; invalidClaims: ClaimValidationError[] }>
  • SessionClaimValidator: ({ claim: SessionClaim<any>; shouldRefetch: any } | {}) & { id: string; validate: any }
    SessionInformation: { customClaimsInAccessTokenPayload: any; expiry: number; recipeUserId: RecipeUserId; sessionDataInDatabase: any; sessionHandle: string; tenantId: string; timeCreated: number; userId: string }

    Type declaration

    • customClaimsInAccessTokenPayload: any
    • expiry: number
    • recipeUserId: RecipeUserId
    • sessionDataInDatabase: any
    • sessionHandle: string
    • tenantId: string
    • timeCreated: number
    • userId: string

    Variables

    Error: typeof default = SessionWrapper.Error

    Functions

    • createJWT(payload?: any, validitySeconds?: number, useStaticSigningKey?: boolean, userContext?: Record<string, any>): Promise<{ jwt: string; status: "OK" } | { status: "UNSUPPORTED_ALGORITHM_ERROR" }>
    • Parameters

      • Optional payload: any
      • Optional validitySeconds: number
      • Optional useStaticSigningKey: boolean
      • Optional userContext: Record<string, any>

      Returns Promise<{ jwt: string; status: "OK" } | { status: "UNSUPPORTED_ALGORITHM_ERROR" }>

    • createNewSession(req: any, res: any, tenantId: string, recipeUserId: RecipeUserId, accessTokenPayload?: any, sessionDataInDatabase?: any, userContext?: Record<string, any>): Promise<SessionContainer>
    • createNewSessionWithoutRequestResponse(tenantId: string, recipeUserId: RecipeUserId, accessTokenPayload?: any, sessionDataInDatabase?: any, disableAntiCsrf?: boolean, userContext?: Record<string, any>): Promise<SessionContainer>
    • fetchAndSetClaim(sessionHandle: string, claim: SessionClaim<any>, userContext?: Record<string, any>): Promise<boolean>
    • getAllSessionHandlesForUser(userId: string, fetchSessionsForAllLinkedAccounts?: boolean, tenantId?: string, userContext?: Record<string, any>): Promise<string[]>
    • Parameters

      • userId: string
      • fetchSessionsForAllLinkedAccounts: boolean = true
      • Optional tenantId: string
      • Optional userContext: Record<string, any>

      Returns Promise<string[]>

    • getClaimValue<T>(sessionHandle: string, claim: SessionClaim<T>, userContext?: Record<string, any>): Promise<{ status: "SESSION_DOES_NOT_EXIST_ERROR" } | { status: "OK"; value: undefined | T }>
    • Type Parameters

      • T

      Parameters

      • sessionHandle: string
      • claim: SessionClaim<T>
      • Optional userContext: Record<string, any>

      Returns Promise<{ status: "SESSION_DOES_NOT_EXIST_ERROR" } | { status: "OK"; value: undefined | T }>

    • getJWKS(userContext?: Record<string, any>): Promise<{ keys: JsonWebKey[] }>
    • getOpenIdDiscoveryConfiguration(userContext?: Record<string, any>): Promise<{ issuer: string; jwks_uri: string; status: "OK" }>
    • getSessionInformation(sessionHandle: string, userContext?: Record<string, any>): Promise<undefined | SessionInformation>
    • getSessionWithoutRequestResponse(accessToken: string, antiCsrfToken?: string): Promise<SessionContainer>
    • getSessionWithoutRequestResponse(accessToken: string, antiCsrfToken?: string, options?: VerifySessionOptions & { sessionRequired?: true }, userContext?: Record<string, any>): Promise<SessionContainer>
    • getSessionWithoutRequestResponse(accessToken: string, antiCsrfToken?: string, options?: VerifySessionOptions & { sessionRequired: false }, userContext?: Record<string, any>): Promise<undefined | SessionContainer>
    • getSessionWithoutRequestResponse(accessToken: string, antiCsrfToken?: string, options?: VerifySessionOptions, userContext?: Record<string, any>): Promise<undefined | SessionContainer>
    • init(config?: TypeInput): RecipeListFunction
    • mergeIntoAccessTokenPayload(sessionHandle: string, accessTokenPayloadUpdate: JSONObject, userContext?: Record<string, any>): Promise<boolean>
    • refreshSession(req: any, res: any, userContext?: Record<string, any>): Promise<SessionContainer>
    • refreshSessionWithoutRequestResponse(refreshToken: string, disableAntiCsrf?: boolean, antiCsrfToken?: string, userContext?: Record<string, any>): Promise<SessionContainer>
    • removeClaim(sessionHandle: string, claim: SessionClaim<any>, userContext?: Record<string, any>): Promise<boolean>
    • revokeAllSessionsForUser(userId: string, revokeSessionsForLinkedAccounts?: boolean, tenantId?: string, userContext?: Record<string, any>): Promise<string[]>
    • Parameters

      • userId: string
      • revokeSessionsForLinkedAccounts: boolean = true
      • Optional tenantId: string
      • Optional userContext: Record<string, any>

      Returns Promise<string[]>

    • revokeMultipleSessions(sessionHandles: string[], userContext?: Record<string, any>): Promise<string[]>
    • revokeSession(sessionHandle: string, userContext?: Record<string, any>): Promise<boolean>
    • setClaimValue<T>(sessionHandle: string, claim: SessionClaim<T>, value: T, userContext?: Record<string, any>): Promise<boolean>
    • Type Parameters

      • T

      Parameters

      • sessionHandle: string
      • claim: SessionClaim<T>
      • value: T
      • Optional userContext: Record<string, any>

      Returns Promise<boolean>

    • updateSessionDataInDatabase(sessionHandle: string, newSessionData: any, userContext?: Record<string, any>): Promise<boolean>

    Legend

    • Variable
    • Function
    • Function with type parameter
    • Type alias
    • Type alias with type parameter
    • Class
    • Class with type parameter
    • Interface

    Settings

    Theme

    Generated using TypeDoc

    \ No newline at end of file diff --git a/docs/modules/recipe_thirdparty.html b/docs/modules/recipe_thirdparty.html index b8ff29461..b49958d51 100644 --- a/docs/modules/recipe_thirdparty.html +++ b/docs/modules/recipe_thirdparty.html @@ -1 +1 @@ -recipe/thirdparty | supertokens-node
    Options
    All
    • Public
    • Public/Protected
    • All
    Menu

    Module recipe/thirdparty

    Index

    Type Aliases

    APIInterface: { appleRedirectHandlerPOST: undefined | ((input: { formPostInfoFromProvider: {}; options: APIOptions; userContext: UserContext }) => Promise<void>); authorisationUrlGET: undefined | ((input: { options: APIOptions; provider: TypeProvider; redirectURIOnProviderDashboard: string; tenantId: string; userContext: UserContext }) => Promise<{ pkceCodeVerifier?: string; status: "OK"; urlWithQueryParams: string } | GeneralErrorResponse>); signInUpPOST: undefined | ((input: { options: APIOptions; provider: TypeProvider; session: SessionContainer | undefined; tenantId: string; userContext: UserContext } & ({ redirectURIInfo: { pkceCodeVerifier?: string; redirectURIOnProviderDashboard: string; redirectURIQueryParams: any } } | { oAuthTokens: {} })) => Promise<{ createdNewRecipeUser: boolean; oAuthTokens: {}; rawUserInfoFromProvider: { fromIdTokenPayload?: {}; fromUserInfoAPI?: {} }; session: SessionContainer; status: "OK"; user: User } | { status: "NO_EMAIL_GIVEN_BY_PROVIDER" } | { reason: string; status: "SIGN_IN_UP_NOT_ALLOWED" } | GeneralErrorResponse>) }

    Type declaration

    • appleRedirectHandlerPOST: undefined | ((input: { formPostInfoFromProvider: {}; options: APIOptions; userContext: UserContext }) => Promise<void>)
    • authorisationUrlGET: undefined | ((input: { options: APIOptions; provider: TypeProvider; redirectURIOnProviderDashboard: string; tenantId: string; userContext: UserContext }) => Promise<{ pkceCodeVerifier?: string; status: "OK"; urlWithQueryParams: string } | GeneralErrorResponse>)
    • signInUpPOST: undefined | ((input: { options: APIOptions; provider: TypeProvider; session: SessionContainer | undefined; tenantId: string; userContext: UserContext } & ({ redirectURIInfo: { pkceCodeVerifier?: string; redirectURIOnProviderDashboard: string; redirectURIQueryParams: any } } | { oAuthTokens: {} })) => Promise<{ createdNewRecipeUser: boolean; oAuthTokens: {}; rawUserInfoFromProvider: { fromIdTokenPayload?: {}; fromUserInfoAPI?: {} }; session: SessionContainer; status: "OK"; user: User } | { status: "NO_EMAIL_GIVEN_BY_PROVIDER" } | { reason: string; status: "SIGN_IN_UP_NOT_ALLOWED" } | GeneralErrorResponse>)
    APIOptions: { appInfo: NormalisedAppinfo; config: TypeNormalisedInput; isInServerlessEnv: boolean; providers: ProviderInput[]; recipeId: string; recipeImplementation: RecipeInterface; req: BaseRequest; res: BaseResponse }

    Type declaration

    • appInfo: NormalisedAppinfo
    • config: TypeNormalisedInput
    • isInServerlessEnv: boolean
    • providers: ProviderInput[]
    • recipeId: string
    • recipeImplementation: RecipeInterface
    • req: BaseRequest
    • res: BaseResponse
    RecipeInterface: { getProvider: any; manuallyCreateOrUpdateUser: any; signInUp: any }

    Type declaration

    • getProvider:function
      • getProvider(input: { clientType?: string; tenantId: string; thirdPartyId: string; userContext: UserContext }): Promise<undefined | TypeProvider>
      • Parameters

        • input: { clientType?: string; tenantId: string; thirdPartyId: string; userContext: UserContext }
          • Optional clientType?: string
          • tenantId: string
          • thirdPartyId: string
          • userContext: UserContext

        Returns Promise<undefined | TypeProvider>

    • manuallyCreateOrUpdateUser:function
      • manuallyCreateOrUpdateUser(input: { email: string; isVerified: boolean; session: SessionContainer | undefined; tenantId: string; thirdPartyId: string; thirdPartyUserId: string; userContext: UserContext }): Promise<{ createdNewRecipeUser: boolean; recipeUserId: RecipeUserId; status: "OK"; user: User } | { reason: string; status: "EMAIL_CHANGE_NOT_ALLOWED_ERROR" } | { reason: string; status: "SIGN_IN_UP_NOT_ALLOWED" } | { reason: "EMAIL_VERIFICATION_REQUIRED" | "RECIPE_USER_ID_ALREADY_LINKED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR" | "ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR" | "SESSION_USER_ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR"; status: "LINKING_TO_SESSION_USER_FAILED" }>
      • Parameters

        • input: { email: string; isVerified: boolean; session: SessionContainer | undefined; tenantId: string; thirdPartyId: string; thirdPartyUserId: string; userContext: UserContext }
          • email: string
          • isVerified: boolean
          • session: SessionContainer | undefined
          • tenantId: string
          • thirdPartyId: string
          • thirdPartyUserId: string
          • userContext: UserContext

        Returns Promise<{ createdNewRecipeUser: boolean; recipeUserId: RecipeUserId; status: "OK"; user: User } | { reason: string; status: "EMAIL_CHANGE_NOT_ALLOWED_ERROR" } | { reason: string; status: "SIGN_IN_UP_NOT_ALLOWED" } | { reason: "EMAIL_VERIFICATION_REQUIRED" | "RECIPE_USER_ID_ALREADY_LINKED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR" | "ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR" | "SESSION_USER_ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR"; status: "LINKING_TO_SESSION_USER_FAILED" }>

    • signInUp:function
      • signInUp(input: { email: string; isVerified: boolean; oAuthTokens: {}; rawUserInfoFromProvider: { fromIdTokenPayload?: {}; fromUserInfoAPI?: {} }; session: SessionContainer | undefined; tenantId: string; thirdPartyId: string; thirdPartyUserId: string; userContext: UserContext }): Promise<{ createdNewRecipeUser: boolean; oAuthTokens: {}; rawUserInfoFromProvider: { fromIdTokenPayload?: {}; fromUserInfoAPI?: {} }; recipeUserId: RecipeUserId; status: "OK"; user: User } | { reason: string; status: "SIGN_IN_UP_NOT_ALLOWED" } | { reason: "EMAIL_VERIFICATION_REQUIRED" | "RECIPE_USER_ID_ALREADY_LINKED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR" | "ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR" | "SESSION_USER_ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR"; status: "LINKING_TO_SESSION_USER_FAILED" }>
      • Parameters

        • input: { email: string; isVerified: boolean; oAuthTokens: {}; rawUserInfoFromProvider: { fromIdTokenPayload?: {}; fromUserInfoAPI?: {} }; session: SessionContainer | undefined; tenantId: string; thirdPartyId: string; thirdPartyUserId: string; userContext: UserContext }
          • email: string
          • isVerified: boolean
          • oAuthTokens: {}
            • [key: string]: any
          • rawUserInfoFromProvider: { fromIdTokenPayload?: {}; fromUserInfoAPI?: {} }
            • Optional fromIdTokenPayload?: {}
              • [key: string]: any
            • Optional fromUserInfoAPI?: {}
              • [key: string]: any
          • session: SessionContainer | undefined
          • tenantId: string
          • thirdPartyId: string
          • thirdPartyUserId: string
          • userContext: UserContext

        Returns Promise<{ createdNewRecipeUser: boolean; oAuthTokens: {}; rawUserInfoFromProvider: { fromIdTokenPayload?: {}; fromUserInfoAPI?: {} }; recipeUserId: RecipeUserId; status: "OK"; user: User } | { reason: string; status: "SIGN_IN_UP_NOT_ALLOWED" } | { reason: "EMAIL_VERIFICATION_REQUIRED" | "RECIPE_USER_ID_ALREADY_LINKED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR" | "ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR" | "SESSION_USER_ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR"; status: "LINKING_TO_SESSION_USER_FAILED" }>

    TypeProvider: { config: ProviderConfigForClientType; id: string; exchangeAuthCodeForOAuthTokens: any; getAuthorisationRedirectURL: any; getConfigForClientType: any; getUserInfo: any }

    Type declaration

    • config: ProviderConfigForClientType
    • id: string
    • exchangeAuthCodeForOAuthTokens:function
      • exchangeAuthCodeForOAuthTokens(input: { redirectURIInfo: { pkceCodeVerifier?: string; redirectURIOnProviderDashboard: string; redirectURIQueryParams: any }; userContext: UserContext }): Promise<any>
      • Parameters

        • input: { redirectURIInfo: { pkceCodeVerifier?: string; redirectURIOnProviderDashboard: string; redirectURIQueryParams: any }; userContext: UserContext }
          • redirectURIInfo: { pkceCodeVerifier?: string; redirectURIOnProviderDashboard: string; redirectURIQueryParams: any }
            • Optional pkceCodeVerifier?: string
            • redirectURIOnProviderDashboard: string
            • redirectURIQueryParams: any
          • userContext: UserContext

        Returns Promise<any>

    • getAuthorisationRedirectURL:function
      • getAuthorisationRedirectURL(input: { redirectURIOnProviderDashboard: string; userContext: UserContext }): Promise<{ pkceCodeVerifier?: string; urlWithQueryParams: string }>
      • Parameters

        • input: { redirectURIOnProviderDashboard: string; userContext: UserContext }
          • redirectURIOnProviderDashboard: string
          • userContext: UserContext

        Returns Promise<{ pkceCodeVerifier?: string; urlWithQueryParams: string }>

    • getConfigForClientType:function
      • getConfigForClientType(input: { clientType?: string; userContext: UserContext }): Promise<ProviderConfigForClientType>
      • Parameters

        • input: { clientType?: string; userContext: UserContext }
          • Optional clientType?: string
          • userContext: UserContext

        Returns Promise<ProviderConfigForClientType>

    • getUserInfo:function
      • getUserInfo(input: { oAuthTokens: any; userContext: UserContext }): Promise<UserInfo>

    Variables

    Error: typeof default = Wrapper.Error

    Functions

    • getProvider(tenantId: string, thirdPartyId: string, clientType: undefined | string, userContext?: Record<string, any>): Promise<undefined | TypeProvider>
    • init(config?: TypeInput): RecipeListFunction
    • manuallyCreateOrUpdateUser(tenantId: string, thirdPartyId: string, thirdPartyUserId: string, email: string, isVerified: boolean, session?: undefined, userContext?: Record<string, any>): Promise<{ createdNewRecipeUser: boolean; recipeUserId: RecipeUserId; status: "OK"; user: User } | { reason: string; status: "EMAIL_CHANGE_NOT_ALLOWED_ERROR" } | { reason: string; status: "SIGN_IN_UP_NOT_ALLOWED" }>
    • manuallyCreateOrUpdateUser(tenantId: string, thirdPartyId: string, thirdPartyUserId: string, email: string, isVerified: boolean, session: SessionContainer, userContext?: Record<string, any>): Promise<{ createdNewRecipeUser: boolean; recipeUserId: RecipeUserId; status: "OK"; user: User } | { reason: string; status: "EMAIL_CHANGE_NOT_ALLOWED_ERROR" } | { reason: string; status: "SIGN_IN_UP_NOT_ALLOWED" } | { reason: "ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR" | "RECIPE_USER_ID_ALREADY_LINKED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR" | "EMAIL_VERIFICATION_REQUIRED" | "SESSION_USER_ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR"; status: "LINKING_TO_SESSION_USER_FAILED" }>
    • Parameters

      • tenantId: string
      • thirdPartyId: string
      • thirdPartyUserId: string
      • email: string
      • isVerified: boolean
      • Optional session: undefined
      • Optional userContext: Record<string, any>

      Returns Promise<{ createdNewRecipeUser: boolean; recipeUserId: RecipeUserId; status: "OK"; user: User } | { reason: string; status: "EMAIL_CHANGE_NOT_ALLOWED_ERROR" } | { reason: string; status: "SIGN_IN_UP_NOT_ALLOWED" }>

    • Parameters

      • tenantId: string
      • thirdPartyId: string
      • thirdPartyUserId: string
      • email: string
      • isVerified: boolean
      • session: SessionContainer
      • Optional userContext: Record<string, any>

      Returns Promise<{ createdNewRecipeUser: boolean; recipeUserId: RecipeUserId; status: "OK"; user: User } | { reason: string; status: "EMAIL_CHANGE_NOT_ALLOWED_ERROR" } | { reason: string; status: "SIGN_IN_UP_NOT_ALLOWED" } | { reason: "ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR" | "RECIPE_USER_ID_ALREADY_LINKED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR" | "EMAIL_VERIFICATION_REQUIRED" | "SESSION_USER_ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR"; status: "LINKING_TO_SESSION_USER_FAILED" }>

    Legend

    • Variable
    • Function
    • Function with type parameter
    • Type alias
    • Type alias with type parameter
    • Class
    • Class with type parameter
    • Interface

    Settings

    Theme

    Generated using TypeDoc

    \ No newline at end of file +recipe/thirdparty | supertokens-node
    Options
    All
    • Public
    • Public/Protected
    • All
    Menu

    Module recipe/thirdparty

    Index

    Type Aliases

    APIInterface: { appleRedirectHandlerPOST: undefined | ((input: { formPostInfoFromProvider: {}; options: APIOptions; userContext: UserContext }) => Promise<void>); authorisationUrlGET: undefined | ((input: { options: APIOptions; provider: TypeProvider; redirectURIOnProviderDashboard: string; tenantId: string; userContext: UserContext }) => Promise<{ pkceCodeVerifier?: string; status: "OK"; urlWithQueryParams: string } | GeneralErrorResponse>); signInUpPOST: undefined | ((input: { options: APIOptions; provider: TypeProvider; session: SessionContainer | undefined; tenantId: string; userContext: UserContext } & ({ redirectURIInfo: { pkceCodeVerifier?: string; redirectURIOnProviderDashboard: string; redirectURIQueryParams: any } } | { oAuthTokens: {} })) => Promise<{ createdNewRecipeUser: boolean; oAuthTokens: {}; rawUserInfoFromProvider: { fromIdTokenPayload?: {}; fromUserInfoAPI?: {} }; session: SessionContainer; status: "OK"; user: User } | { status: "NO_EMAIL_GIVEN_BY_PROVIDER" } | { reason: string; status: "SIGN_IN_UP_NOT_ALLOWED" } | GeneralErrorResponse>) }

    Type declaration

    • appleRedirectHandlerPOST: undefined | ((input: { formPostInfoFromProvider: {}; options: APIOptions; userContext: UserContext }) => Promise<void>)
    • authorisationUrlGET: undefined | ((input: { options: APIOptions; provider: TypeProvider; redirectURIOnProviderDashboard: string; tenantId: string; userContext: UserContext }) => Promise<{ pkceCodeVerifier?: string; status: "OK"; urlWithQueryParams: string } | GeneralErrorResponse>)
    • signInUpPOST: undefined | ((input: { options: APIOptions; provider: TypeProvider; session: SessionContainer | undefined; tenantId: string; userContext: UserContext } & ({ redirectURIInfo: { pkceCodeVerifier?: string; redirectURIOnProviderDashboard: string; redirectURIQueryParams: any } } | { oAuthTokens: {} })) => Promise<{ createdNewRecipeUser: boolean; oAuthTokens: {}; rawUserInfoFromProvider: { fromIdTokenPayload?: {}; fromUserInfoAPI?: {} }; session: SessionContainer; status: "OK"; user: User } | { status: "NO_EMAIL_GIVEN_BY_PROVIDER" } | { reason: string; status: "SIGN_IN_UP_NOT_ALLOWED" } | GeneralErrorResponse>)
    APIOptions: { appInfo: NormalisedAppinfo; config: TypeNormalisedInput; isInServerlessEnv: boolean; providers: ProviderInput[]; recipeId: string; recipeImplementation: RecipeInterface; req: BaseRequest; res: BaseResponse }

    Type declaration

    • appInfo: NormalisedAppinfo
    • config: TypeNormalisedInput
    • isInServerlessEnv: boolean
    • providers: ProviderInput[]
    • recipeId: string
    • recipeImplementation: RecipeInterface
    • req: BaseRequest
    • res: BaseResponse
    RecipeInterface: { getProvider: any; manuallyCreateOrUpdateUser: any; signInUp: any }

    Type declaration

    • getProvider:function
      • getProvider(input: { clientType?: string; tenantId: string; thirdPartyId: string; userContext: UserContext }): Promise<undefined | TypeProvider>
      • Parameters

        • input: { clientType?: string; tenantId: string; thirdPartyId: string; userContext: UserContext }
          • Optional clientType?: string
          • tenantId: string
          • thirdPartyId: string
          • userContext: UserContext

        Returns Promise<undefined | TypeProvider>

    • manuallyCreateOrUpdateUser:function
      • manuallyCreateOrUpdateUser(input: { email: string; isVerified: boolean; session: SessionContainer | undefined; tenantId: string; thirdPartyId: string; thirdPartyUserId: string; userContext: UserContext }): Promise<{ createdNewRecipeUser: boolean; recipeUserId: RecipeUserId; status: "OK"; user: User } | { reason: string; status: "EMAIL_CHANGE_NOT_ALLOWED_ERROR" } | { reason: string; status: "SIGN_IN_UP_NOT_ALLOWED" } | { reason: "EMAIL_VERIFICATION_REQUIRED" | "RECIPE_USER_ID_ALREADY_LINKED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR" | "ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR" | "SESSION_USER_ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR"; status: "LINKING_TO_SESSION_USER_FAILED" }>
      • Parameters

        • input: { email: string; isVerified: boolean; session: SessionContainer | undefined; tenantId: string; thirdPartyId: string; thirdPartyUserId: string; userContext: UserContext }
          • email: string
          • isVerified: boolean
          • session: SessionContainer | undefined
          • tenantId: string
          • thirdPartyId: string
          • thirdPartyUserId: string
          • userContext: UserContext

        Returns Promise<{ createdNewRecipeUser: boolean; recipeUserId: RecipeUserId; status: "OK"; user: User } | { reason: string; status: "EMAIL_CHANGE_NOT_ALLOWED_ERROR" } | { reason: string; status: "SIGN_IN_UP_NOT_ALLOWED" } | { reason: "EMAIL_VERIFICATION_REQUIRED" | "RECIPE_USER_ID_ALREADY_LINKED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR" | "ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR" | "SESSION_USER_ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR"; status: "LINKING_TO_SESSION_USER_FAILED" }>

    • signInUp:function
      • signInUp(input: { email: string; isVerified: boolean; oAuthTokens: {}; rawUserInfoFromProvider: { fromIdTokenPayload?: {}; fromUserInfoAPI?: {} }; session: SessionContainer | undefined; tenantId: string; thirdPartyId: string; thirdPartyUserId: string; userContext: UserContext }): Promise<{ createdNewRecipeUser: boolean; oAuthTokens: {}; rawUserInfoFromProvider: { fromIdTokenPayload?: {}; fromUserInfoAPI?: {} }; recipeUserId: RecipeUserId; status: "OK"; user: User } | { reason: string; status: "SIGN_IN_UP_NOT_ALLOWED" } | { reason: "EMAIL_VERIFICATION_REQUIRED" | "RECIPE_USER_ID_ALREADY_LINKED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR" | "ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR" | "SESSION_USER_ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR"; status: "LINKING_TO_SESSION_USER_FAILED" }>
      • Parameters

        • input: { email: string; isVerified: boolean; oAuthTokens: {}; rawUserInfoFromProvider: { fromIdTokenPayload?: {}; fromUserInfoAPI?: {} }; session: SessionContainer | undefined; tenantId: string; thirdPartyId: string; thirdPartyUserId: string; userContext: UserContext }
          • email: string
          • isVerified: boolean
          • oAuthTokens: {}
            • [key: string]: any
          • rawUserInfoFromProvider: { fromIdTokenPayload?: {}; fromUserInfoAPI?: {} }
            • Optional fromIdTokenPayload?: {}
              • [key: string]: any
            • Optional fromUserInfoAPI?: {}
              • [key: string]: any
          • session: SessionContainer | undefined
          • tenantId: string
          • thirdPartyId: string
          • thirdPartyUserId: string
          • userContext: UserContext

        Returns Promise<{ createdNewRecipeUser: boolean; oAuthTokens: {}; rawUserInfoFromProvider: { fromIdTokenPayload?: {}; fromUserInfoAPI?: {} }; recipeUserId: RecipeUserId; status: "OK"; user: User } | { reason: string; status: "SIGN_IN_UP_NOT_ALLOWED" } | { reason: "EMAIL_VERIFICATION_REQUIRED" | "RECIPE_USER_ID_ALREADY_LINKED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR" | "ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR" | "SESSION_USER_ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR"; status: "LINKING_TO_SESSION_USER_FAILED" }>

    TypeProvider: { config: ProviderConfigForClientType; id: string; exchangeAuthCodeForOAuthTokens: any; getAuthorisationRedirectURL: any; getConfigForClientType: any; getUserInfo: any }

    Type declaration

    • config: ProviderConfigForClientType
    • id: string
    • exchangeAuthCodeForOAuthTokens:function
      • exchangeAuthCodeForOAuthTokens(input: { redirectURIInfo: { pkceCodeVerifier?: string; redirectURIOnProviderDashboard: string; redirectURIQueryParams: any }; userContext: UserContext }): Promise<any>
      • Parameters

        • input: { redirectURIInfo: { pkceCodeVerifier?: string; redirectURIOnProviderDashboard: string; redirectURIQueryParams: any }; userContext: UserContext }
          • redirectURIInfo: { pkceCodeVerifier?: string; redirectURIOnProviderDashboard: string; redirectURIQueryParams: any }
            • Optional pkceCodeVerifier?: string
            • redirectURIOnProviderDashboard: string
            • redirectURIQueryParams: any
          • userContext: UserContext

        Returns Promise<any>

    • getAuthorisationRedirectURL:function
      • getAuthorisationRedirectURL(input: { redirectURIOnProviderDashboard: string; userContext: UserContext }): Promise<{ pkceCodeVerifier?: string; urlWithQueryParams: string }>
      • Parameters

        • input: { redirectURIOnProviderDashboard: string; userContext: UserContext }
          • redirectURIOnProviderDashboard: string
          • userContext: UserContext

        Returns Promise<{ pkceCodeVerifier?: string; urlWithQueryParams: string }>

    • getConfigForClientType:function
      • getConfigForClientType(input: { clientType?: string; userContext: UserContext }): Promise<ProviderConfigForClientType>
      • Parameters

        • input: { clientType?: string; userContext: UserContext }
          • Optional clientType?: string
          • userContext: UserContext

        Returns Promise<ProviderConfigForClientType>

    • getUserInfo:function
      • getUserInfo(input: { oAuthTokens: any; userContext: UserContext }): Promise<UserInfo>

    Variables

    Error: typeof default = Wrapper.Error

    Functions

    • getProvider(tenantId: string, thirdPartyId: string, clientType: undefined | string, userContext?: Record<string, any>): Promise<undefined | TypeProvider>
    • init(config?: TypeInput): RecipeListFunction
    • manuallyCreateOrUpdateUser(tenantId: string, thirdPartyId: string, thirdPartyUserId: string, email: string, isVerified: boolean, session?: undefined, userContext?: Record<string, any>): Promise<{ createdNewRecipeUser: boolean; recipeUserId: RecipeUserId; status: "OK"; user: User } | { reason: string; status: "EMAIL_CHANGE_NOT_ALLOWED_ERROR" } | { reason: string; status: "SIGN_IN_UP_NOT_ALLOWED" }>
    • manuallyCreateOrUpdateUser(tenantId: string, thirdPartyId: string, thirdPartyUserId: string, email: string, isVerified: boolean, session: SessionContainer, userContext?: Record<string, any>): Promise<{ createdNewRecipeUser: boolean; recipeUserId: RecipeUserId; status: "OK"; user: User } | { reason: string; status: "EMAIL_CHANGE_NOT_ALLOWED_ERROR" } | { reason: string; status: "SIGN_IN_UP_NOT_ALLOWED" } | { reason: "ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR" | "RECIPE_USER_ID_ALREADY_LINKED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR" | "EMAIL_VERIFICATION_REQUIRED" | "SESSION_USER_ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR"; status: "LINKING_TO_SESSION_USER_FAILED" }>
    • Parameters

      • tenantId: string
      • thirdPartyId: string
      • thirdPartyUserId: string
      • email: string
      • isVerified: boolean
      • Optional session: undefined
      • Optional userContext: Record<string, any>

      Returns Promise<{ createdNewRecipeUser: boolean; recipeUserId: RecipeUserId; status: "OK"; user: User } | { reason: string; status: "EMAIL_CHANGE_NOT_ALLOWED_ERROR" } | { reason: string; status: "SIGN_IN_UP_NOT_ALLOWED" }>

    • Parameters

      • tenantId: string
      • thirdPartyId: string
      • thirdPartyUserId: string
      • email: string
      • isVerified: boolean
      • session: SessionContainer
      • Optional userContext: Record<string, any>

      Returns Promise<{ createdNewRecipeUser: boolean; recipeUserId: RecipeUserId; status: "OK"; user: User } | { reason: string; status: "EMAIL_CHANGE_NOT_ALLOWED_ERROR" } | { reason: string; status: "SIGN_IN_UP_NOT_ALLOWED" } | { reason: "ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR" | "RECIPE_USER_ID_ALREADY_LINKED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR" | "EMAIL_VERIFICATION_REQUIRED" | "SESSION_USER_ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR"; status: "LINKING_TO_SESSION_USER_FAILED" }>

    Legend

    • Variable
    • Function
    • Function with type parameter
    • Type alias
    • Type alias with type parameter
    • Class
    • Class with type parameter
    • Interface

    Settings

    Theme

    Generated using TypeDoc

    \ No newline at end of file diff --git a/docs/modules/recipe_totp.html b/docs/modules/recipe_totp.html index b5f1d1bf7..a4448fbe8 100644 --- a/docs/modules/recipe_totp.html +++ b/docs/modules/recipe_totp.html @@ -1 +1 @@ -recipe/totp | supertokens-node
    Options
    All
    • Public
    • Public/Protected
    • All
    Menu

    Module recipe/totp

    Index

    Type Aliases

    APIInterface: { createDevicePOST: undefined | ((input: { deviceName?: string; options: APIOptions; session: SessionContainer; userContext: UserContext }) => Promise<{ deviceName: string; qrCodeString: string; secret: string; status: "OK" } | { status: "DEVICE_ALREADY_EXISTS_ERROR" } | GeneralErrorResponse>); listDevicesGET: undefined | ((input: { options: APIOptions; session: SessionContainer; userContext: UserContext }) => Promise<{ devices: { name: string; period: number; skew: number; verified: boolean }[]; status: "OK" } | GeneralErrorResponse>); removeDevicePOST: undefined | ((input: { deviceName: string; options: APIOptions; session: SessionContainer; userContext: UserContext }) => Promise<{ didDeviceExist: boolean; status: "OK" } | GeneralErrorResponse>); verifyDevicePOST: undefined | ((input: { deviceName: string; options: APIOptions; session: SessionContainer; totp: string; userContext: UserContext }) => Promise<{ status: "OK"; wasAlreadyVerified: boolean } | { status: "UNKNOWN_DEVICE_ERROR" } | { currentNumberOfFailedAttempts: number; maxNumberOfFailedAttempts: number; status: "INVALID_TOTP_ERROR" } | { retryAfterMs: number; status: "LIMIT_REACHED_ERROR" } | GeneralErrorResponse>); verifyTOTPPOST: undefined | ((input: { options: APIOptions; session: SessionContainer; totp: string; userContext: UserContext }) => Promise<{ status: "OK" | "UNKNOWN_USER_ID_ERROR" } | { currentNumberOfFailedAttempts: number; maxNumberOfFailedAttempts: number; status: "INVALID_TOTP_ERROR" } | { retryAfterMs: number; status: "LIMIT_REACHED_ERROR" } | GeneralErrorResponse>) }

    Type declaration

    • createDevicePOST: undefined | ((input: { deviceName?: string; options: APIOptions; session: SessionContainer; userContext: UserContext }) => Promise<{ deviceName: string; qrCodeString: string; secret: string; status: "OK" } | { status: "DEVICE_ALREADY_EXISTS_ERROR" } | GeneralErrorResponse>)
    • listDevicesGET: undefined | ((input: { options: APIOptions; session: SessionContainer; userContext: UserContext }) => Promise<{ devices: { name: string; period: number; skew: number; verified: boolean }[]; status: "OK" } | GeneralErrorResponse>)
    • removeDevicePOST: undefined | ((input: { deviceName: string; options: APIOptions; session: SessionContainer; userContext: UserContext }) => Promise<{ didDeviceExist: boolean; status: "OK" } | GeneralErrorResponse>)
    • verifyDevicePOST: undefined | ((input: { deviceName: string; options: APIOptions; session: SessionContainer; totp: string; userContext: UserContext }) => Promise<{ status: "OK"; wasAlreadyVerified: boolean } | { status: "UNKNOWN_DEVICE_ERROR" } | { currentNumberOfFailedAttempts: number; maxNumberOfFailedAttempts: number; status: "INVALID_TOTP_ERROR" } | { retryAfterMs: number; status: "LIMIT_REACHED_ERROR" } | GeneralErrorResponse>)
    • verifyTOTPPOST: undefined | ((input: { options: APIOptions; session: SessionContainer; totp: string; userContext: UserContext }) => Promise<{ status: "OK" | "UNKNOWN_USER_ID_ERROR" } | { currentNumberOfFailedAttempts: number; maxNumberOfFailedAttempts: number; status: "INVALID_TOTP_ERROR" } | { retryAfterMs: number; status: "LIMIT_REACHED_ERROR" } | GeneralErrorResponse>)
    APIOptions: { config: TypeNormalisedInput; isInServerlessEnv: boolean; recipeId: string; recipeImplementation: RecipeInterface; req: BaseRequest; res: BaseResponse }

    Type declaration

    RecipeInterface: { createDevice: any; getUserIdentifierInfoForUserId: any; listDevices: any; removeDevice: any; updateDevice: any; verifyDevice: any; verifyTOTP: any }

    Type declaration

    • createDevice:function
      • createDevice(input: { deviceName?: string; period?: number; skew?: number; userContext: UserContext; userId: string; userIdentifierInfo?: string }): Promise<{ deviceName: string; qrCodeString: string; secret: string; status: "OK" } | { status: "DEVICE_ALREADY_EXISTS_ERROR" } | { status: "UNKNOWN_USER_ID_ERROR" }>
      • Parameters

        • input: { deviceName?: string; period?: number; skew?: number; userContext: UserContext; userId: string; userIdentifierInfo?: string }
          • Optional deviceName?: string
          • Optional period?: number
          • Optional skew?: number
          • userContext: UserContext
          • userId: string
          • Optional userIdentifierInfo?: string

        Returns Promise<{ deviceName: string; qrCodeString: string; secret: string; status: "OK" } | { status: "DEVICE_ALREADY_EXISTS_ERROR" } | { status: "UNKNOWN_USER_ID_ERROR" }>

    • getUserIdentifierInfoForUserId:function
      • getUserIdentifierInfoForUserId(input: { userContext: UserContext; userId: string }): Promise<{ info: string; status: "OK" } | { status: "UNKNOWN_USER_ID_ERROR" | "USER_IDENTIFIER_INFO_DOES_NOT_EXIST_ERROR" }>
      • Parameters

        • input: { userContext: UserContext; userId: string }
          • userContext: UserContext
          • userId: string

        Returns Promise<{ info: string; status: "OK" } | { status: "UNKNOWN_USER_ID_ERROR" | "USER_IDENTIFIER_INFO_DOES_NOT_EXIST_ERROR" }>

    • listDevices:function
      • listDevices(input: { userContext: UserContext; userId: string }): Promise<{ devices: { name: string; period: number; skew: number; verified: boolean }[]; status: "OK" }>
      • Parameters

        • input: { userContext: UserContext; userId: string }
          • userContext: UserContext
          • userId: string

        Returns Promise<{ devices: { name: string; period: number; skew: number; verified: boolean }[]; status: "OK" }>

    • removeDevice:function
      • removeDevice(input: { deviceName: string; userContext: UserContext; userId: string }): Promise<{ didDeviceExist: boolean; status: "OK" }>
      • Parameters

        • input: { deviceName: string; userContext: UserContext; userId: string }
          • deviceName: string
          • userContext: UserContext
          • userId: string

        Returns Promise<{ didDeviceExist: boolean; status: "OK" }>

    • updateDevice:function
      • updateDevice(input: { existingDeviceName: string; newDeviceName: string; userContext: UserContext; userId: string }): Promise<{ status: "OK" | "UNKNOWN_DEVICE_ERROR" | "DEVICE_ALREADY_EXISTS_ERROR" }>
      • Parameters

        • input: { existingDeviceName: string; newDeviceName: string; userContext: UserContext; userId: string }
          • existingDeviceName: string
          • newDeviceName: string
          • userContext: UserContext
          • userId: string

        Returns Promise<{ status: "OK" | "UNKNOWN_DEVICE_ERROR" | "DEVICE_ALREADY_EXISTS_ERROR" }>

    • verifyDevice:function
      • verifyDevice(input: { deviceName: string; tenantId: string; totp: string; userContext: UserContext; userId: string }): Promise<{ status: "OK"; wasAlreadyVerified: boolean } | { status: "UNKNOWN_DEVICE_ERROR" } | { currentNumberOfFailedAttempts: number; maxNumberOfFailedAttempts: number; status: "INVALID_TOTP_ERROR" } | { retryAfterMs: number; status: "LIMIT_REACHED_ERROR" }>
      • Parameters

        • input: { deviceName: string; tenantId: string; totp: string; userContext: UserContext; userId: string }
          • deviceName: string
          • tenantId: string
          • totp: string
          • userContext: UserContext
          • userId: string

        Returns Promise<{ status: "OK"; wasAlreadyVerified: boolean } | { status: "UNKNOWN_DEVICE_ERROR" } | { currentNumberOfFailedAttempts: number; maxNumberOfFailedAttempts: number; status: "INVALID_TOTP_ERROR" } | { retryAfterMs: number; status: "LIMIT_REACHED_ERROR" }>

    • verifyTOTP:function
      • verifyTOTP(input: { tenantId: string; totp: string; userContext: UserContext; userId: string }): Promise<{ status: "OK" | "UNKNOWN_USER_ID_ERROR" } | { currentNumberOfFailedAttempts: number; maxNumberOfFailedAttempts: number; status: "INVALID_TOTP_ERROR" } | { retryAfterMs: number; status: "LIMIT_REACHED_ERROR" }>
      • Parameters

        • input: { tenantId: string; totp: string; userContext: UserContext; userId: string }
          • tenantId: string
          • totp: string
          • userContext: UserContext
          • userId: string

        Returns Promise<{ status: "OK" | "UNKNOWN_USER_ID_ERROR" } | { currentNumberOfFailedAttempts: number; maxNumberOfFailedAttempts: number; status: "INVALID_TOTP_ERROR" } | { retryAfterMs: number; status: "LIMIT_REACHED_ERROR" }>

    Functions

    • createDevice(userId: string, userIdentifierInfo?: string, deviceName?: string, skew?: number, period?: number, userContext?: Record<string, any>): Promise<{ deviceName: string; qrCodeString: string; secret: string; status: "OK" } | { status: "DEVICE_ALREADY_EXISTS_ERROR" } | { status: "UNKNOWN_USER_ID_ERROR" }>
    • Parameters

      • userId: string
      • Optional userIdentifierInfo: string
      • Optional deviceName: string
      • Optional skew: number
      • Optional period: number
      • Optional userContext: Record<string, any>

      Returns Promise<{ deviceName: string; qrCodeString: string; secret: string; status: "OK" } | { status: "DEVICE_ALREADY_EXISTS_ERROR" } | { status: "UNKNOWN_USER_ID_ERROR" }>

    • init(config?: TypeInput): RecipeListFunction
    • listDevices(userId: string, userContext?: Record<string, any>): Promise<{ devices: { name: string; period: number; skew: number; verified: boolean }[]; status: "OK" }>
    • Parameters

      • userId: string
      • Optional userContext: Record<string, any>

      Returns Promise<{ devices: { name: string; period: number; skew: number; verified: boolean }[]; status: "OK" }>

    • removeDevice(userId: string, deviceName: string, userContext?: Record<string, any>): Promise<{ didDeviceExist: boolean; status: "OK" }>
    • Parameters

      • userId: string
      • deviceName: string
      • Optional userContext: Record<string, any>

      Returns Promise<{ didDeviceExist: boolean; status: "OK" }>

    • updateDevice(userId: string, existingDeviceName: string, newDeviceName: string, userContext?: Record<string, any>): Promise<{ status: "OK" | "DEVICE_ALREADY_EXISTS_ERROR" | "UNKNOWN_DEVICE_ERROR" }>
    • Parameters

      • userId: string
      • existingDeviceName: string
      • newDeviceName: string
      • Optional userContext: Record<string, any>

      Returns Promise<{ status: "OK" | "DEVICE_ALREADY_EXISTS_ERROR" | "UNKNOWN_DEVICE_ERROR" }>

    • verifyDevice(tenantId: string, userId: string, deviceName: string, totp: string, userContext?: Record<string, any>): Promise<{ status: "OK"; wasAlreadyVerified: boolean } | { status: "UNKNOWN_DEVICE_ERROR" } | { currentNumberOfFailedAttempts: number; maxNumberOfFailedAttempts: number; status: "INVALID_TOTP_ERROR" } | { retryAfterMs: number; status: "LIMIT_REACHED_ERROR" }>
    • Parameters

      • tenantId: string
      • userId: string
      • deviceName: string
      • totp: string
      • Optional userContext: Record<string, any>

      Returns Promise<{ status: "OK"; wasAlreadyVerified: boolean } | { status: "UNKNOWN_DEVICE_ERROR" } | { currentNumberOfFailedAttempts: number; maxNumberOfFailedAttempts: number; status: "INVALID_TOTP_ERROR" } | { retryAfterMs: number; status: "LIMIT_REACHED_ERROR" }>

    • verifyTOTP(tenantId: string, userId: string, totp: string, userContext?: Record<string, any>): Promise<{ status: "OK" | "UNKNOWN_USER_ID_ERROR" } | { currentNumberOfFailedAttempts: number; maxNumberOfFailedAttempts: number; status: "INVALID_TOTP_ERROR" } | { retryAfterMs: number; status: "LIMIT_REACHED_ERROR" }>
    • Parameters

      • tenantId: string
      • userId: string
      • totp: string
      • Optional userContext: Record<string, any>

      Returns Promise<{ status: "OK" | "UNKNOWN_USER_ID_ERROR" } | { currentNumberOfFailedAttempts: number; maxNumberOfFailedAttempts: number; status: "INVALID_TOTP_ERROR" } | { retryAfterMs: number; status: "LIMIT_REACHED_ERROR" }>

    Legend

    • Variable
    • Function
    • Function with type parameter
    • Type alias
    • Type alias with type parameter
    • Class
    • Class with type parameter
    • Interface

    Settings

    Theme

    Generated using TypeDoc

    \ No newline at end of file +recipe/totp | supertokens-node
    Options
    All
    • Public
    • Public/Protected
    • All
    Menu

    Module recipe/totp

    Index

    Type Aliases

    APIInterface: { createDevicePOST: undefined | ((input: { deviceName?: string; options: APIOptions; session: SessionContainer; userContext: UserContext }) => Promise<{ deviceName: string; qrCodeString: string; secret: string; status: "OK" } | { status: "DEVICE_ALREADY_EXISTS_ERROR" } | GeneralErrorResponse>); listDevicesGET: undefined | ((input: { options: APIOptions; session: SessionContainer; userContext: UserContext }) => Promise<{ devices: { name: string; period: number; skew: number; verified: boolean }[]; status: "OK" } | GeneralErrorResponse>); removeDevicePOST: undefined | ((input: { deviceName: string; options: APIOptions; session: SessionContainer; userContext: UserContext }) => Promise<{ didDeviceExist: boolean; status: "OK" } | GeneralErrorResponse>); verifyDevicePOST: undefined | ((input: { deviceName: string; options: APIOptions; session: SessionContainer; totp: string; userContext: UserContext }) => Promise<{ status: "OK"; wasAlreadyVerified: boolean } | { status: "UNKNOWN_DEVICE_ERROR" } | { currentNumberOfFailedAttempts: number; maxNumberOfFailedAttempts: number; status: "INVALID_TOTP_ERROR" } | { retryAfterMs: number; status: "LIMIT_REACHED_ERROR" } | GeneralErrorResponse>); verifyTOTPPOST: undefined | ((input: { options: APIOptions; session: SessionContainer; totp: string; userContext: UserContext }) => Promise<{ status: "OK" | "UNKNOWN_USER_ID_ERROR" } | { currentNumberOfFailedAttempts: number; maxNumberOfFailedAttempts: number; status: "INVALID_TOTP_ERROR" } | { retryAfterMs: number; status: "LIMIT_REACHED_ERROR" } | GeneralErrorResponse>) }

    Type declaration

    • createDevicePOST: undefined | ((input: { deviceName?: string; options: APIOptions; session: SessionContainer; userContext: UserContext }) => Promise<{ deviceName: string; qrCodeString: string; secret: string; status: "OK" } | { status: "DEVICE_ALREADY_EXISTS_ERROR" } | GeneralErrorResponse>)
    • listDevicesGET: undefined | ((input: { options: APIOptions; session: SessionContainer; userContext: UserContext }) => Promise<{ devices: { name: string; period: number; skew: number; verified: boolean }[]; status: "OK" } | GeneralErrorResponse>)
    • removeDevicePOST: undefined | ((input: { deviceName: string; options: APIOptions; session: SessionContainer; userContext: UserContext }) => Promise<{ didDeviceExist: boolean; status: "OK" } | GeneralErrorResponse>)
    • verifyDevicePOST: undefined | ((input: { deviceName: string; options: APIOptions; session: SessionContainer; totp: string; userContext: UserContext }) => Promise<{ status: "OK"; wasAlreadyVerified: boolean } | { status: "UNKNOWN_DEVICE_ERROR" } | { currentNumberOfFailedAttempts: number; maxNumberOfFailedAttempts: number; status: "INVALID_TOTP_ERROR" } | { retryAfterMs: number; status: "LIMIT_REACHED_ERROR" } | GeneralErrorResponse>)
    • verifyTOTPPOST: undefined | ((input: { options: APIOptions; session: SessionContainer; totp: string; userContext: UserContext }) => Promise<{ status: "OK" | "UNKNOWN_USER_ID_ERROR" } | { currentNumberOfFailedAttempts: number; maxNumberOfFailedAttempts: number; status: "INVALID_TOTP_ERROR" } | { retryAfterMs: number; status: "LIMIT_REACHED_ERROR" } | GeneralErrorResponse>)
    APIOptions: { config: TypeNormalisedInput; isInServerlessEnv: boolean; recipeId: string; recipeImplementation: RecipeInterface; req: BaseRequest; res: BaseResponse }

    Type declaration

    RecipeInterface: { createDevice: any; getUserIdentifierInfoForUserId: any; listDevices: any; removeDevice: any; updateDevice: any; verifyDevice: any; verifyTOTP: any }

    Type declaration

    • createDevice:function
      • createDevice(input: { deviceName?: string; period?: number; skew?: number; userContext: UserContext; userId: string; userIdentifierInfo?: string }): Promise<{ deviceName: string; qrCodeString: string; secret: string; status: "OK" } | { status: "DEVICE_ALREADY_EXISTS_ERROR" } | { status: "UNKNOWN_USER_ID_ERROR" }>
      • Parameters

        • input: { deviceName?: string; period?: number; skew?: number; userContext: UserContext; userId: string; userIdentifierInfo?: string }
          • Optional deviceName?: string
          • Optional period?: number
          • Optional skew?: number
          • userContext: UserContext
          • userId: string
          • Optional userIdentifierInfo?: string

        Returns Promise<{ deviceName: string; qrCodeString: string; secret: string; status: "OK" } | { status: "DEVICE_ALREADY_EXISTS_ERROR" } | { status: "UNKNOWN_USER_ID_ERROR" }>

    • getUserIdentifierInfoForUserId:function
      • getUserIdentifierInfoForUserId(input: { userContext: UserContext; userId: string }): Promise<{ info: string; status: "OK" } | { status: "UNKNOWN_USER_ID_ERROR" | "USER_IDENTIFIER_INFO_DOES_NOT_EXIST_ERROR" }>
      • Parameters

        • input: { userContext: UserContext; userId: string }
          • userContext: UserContext
          • userId: string

        Returns Promise<{ info: string; status: "OK" } | { status: "UNKNOWN_USER_ID_ERROR" | "USER_IDENTIFIER_INFO_DOES_NOT_EXIST_ERROR" }>

    • listDevices:function
      • listDevices(input: { userContext: UserContext; userId: string }): Promise<{ devices: { name: string; period: number; skew: number; verified: boolean }[]; status: "OK" }>
      • Parameters

        • input: { userContext: UserContext; userId: string }
          • userContext: UserContext
          • userId: string

        Returns Promise<{ devices: { name: string; period: number; skew: number; verified: boolean }[]; status: "OK" }>

    • removeDevice:function
      • removeDevice(input: { deviceName: string; userContext: UserContext; userId: string }): Promise<{ didDeviceExist: boolean; status: "OK" }>
      • Parameters

        • input: { deviceName: string; userContext: UserContext; userId: string }
          • deviceName: string
          • userContext: UserContext
          • userId: string

        Returns Promise<{ didDeviceExist: boolean; status: "OK" }>

    • updateDevice:function
      • updateDevice(input: { existingDeviceName: string; newDeviceName: string; userContext: UserContext; userId: string }): Promise<{ status: "OK" | "UNKNOWN_DEVICE_ERROR" | "DEVICE_ALREADY_EXISTS_ERROR" }>
      • Parameters

        • input: { existingDeviceName: string; newDeviceName: string; userContext: UserContext; userId: string }
          • existingDeviceName: string
          • newDeviceName: string
          • userContext: UserContext
          • userId: string

        Returns Promise<{ status: "OK" | "UNKNOWN_DEVICE_ERROR" | "DEVICE_ALREADY_EXISTS_ERROR" }>

    • verifyDevice:function
      • verifyDevice(input: { deviceName: string; tenantId: string; totp: string; userContext: UserContext; userId: string }): Promise<{ status: "OK"; wasAlreadyVerified: boolean } | { status: "UNKNOWN_DEVICE_ERROR" } | { currentNumberOfFailedAttempts: number; maxNumberOfFailedAttempts: number; status: "INVALID_TOTP_ERROR" } | { retryAfterMs: number; status: "LIMIT_REACHED_ERROR" }>
      • Parameters

        • input: { deviceName: string; tenantId: string; totp: string; userContext: UserContext; userId: string }
          • deviceName: string
          • tenantId: string
          • totp: string
          • userContext: UserContext
          • userId: string

        Returns Promise<{ status: "OK"; wasAlreadyVerified: boolean } | { status: "UNKNOWN_DEVICE_ERROR" } | { currentNumberOfFailedAttempts: number; maxNumberOfFailedAttempts: number; status: "INVALID_TOTP_ERROR" } | { retryAfterMs: number; status: "LIMIT_REACHED_ERROR" }>

    • verifyTOTP:function
      • verifyTOTP(input: { tenantId: string; totp: string; userContext: UserContext; userId: string }): Promise<{ status: "OK" | "UNKNOWN_USER_ID_ERROR" } | { currentNumberOfFailedAttempts: number; maxNumberOfFailedAttempts: number; status: "INVALID_TOTP_ERROR" } | { retryAfterMs: number; status: "LIMIT_REACHED_ERROR" }>
      • Parameters

        • input: { tenantId: string; totp: string; userContext: UserContext; userId: string }
          • tenantId: string
          • totp: string
          • userContext: UserContext
          • userId: string

        Returns Promise<{ status: "OK" | "UNKNOWN_USER_ID_ERROR" } | { currentNumberOfFailedAttempts: number; maxNumberOfFailedAttempts: number; status: "INVALID_TOTP_ERROR" } | { retryAfterMs: number; status: "LIMIT_REACHED_ERROR" }>

    Functions

    • createDevice(userId: string, userIdentifierInfo?: string, deviceName?: string, skew?: number, period?: number, userContext?: Record<string, any>): Promise<{ deviceName: string; qrCodeString: string; secret: string; status: "OK" } | { status: "DEVICE_ALREADY_EXISTS_ERROR" } | { status: "UNKNOWN_USER_ID_ERROR" }>
    • Parameters

      • userId: string
      • Optional userIdentifierInfo: string
      • Optional deviceName: string
      • Optional skew: number
      • Optional period: number
      • Optional userContext: Record<string, any>

      Returns Promise<{ deviceName: string; qrCodeString: string; secret: string; status: "OK" } | { status: "DEVICE_ALREADY_EXISTS_ERROR" } | { status: "UNKNOWN_USER_ID_ERROR" }>

    • init(config?: TypeInput): RecipeListFunction
    • listDevices(userId: string, userContext?: Record<string, any>): Promise<{ devices: { name: string; period: number; skew: number; verified: boolean }[]; status: "OK" }>
    • Parameters

      • userId: string
      • Optional userContext: Record<string, any>

      Returns Promise<{ devices: { name: string; period: number; skew: number; verified: boolean }[]; status: "OK" }>

    • removeDevice(userId: string, deviceName: string, userContext?: Record<string, any>): Promise<{ didDeviceExist: boolean; status: "OK" }>
    • Parameters

      • userId: string
      • deviceName: string
      • Optional userContext: Record<string, any>

      Returns Promise<{ didDeviceExist: boolean; status: "OK" }>

    • updateDevice(userId: string, existingDeviceName: string, newDeviceName: string, userContext?: Record<string, any>): Promise<{ status: "OK" | "DEVICE_ALREADY_EXISTS_ERROR" | "UNKNOWN_DEVICE_ERROR" }>
    • Parameters

      • userId: string
      • existingDeviceName: string
      • newDeviceName: string
      • Optional userContext: Record<string, any>

      Returns Promise<{ status: "OK" | "DEVICE_ALREADY_EXISTS_ERROR" | "UNKNOWN_DEVICE_ERROR" }>

    • verifyDevice(tenantId: string, userId: string, deviceName: string, totp: string, userContext?: Record<string, any>): Promise<{ status: "OK"; wasAlreadyVerified: boolean } | { status: "UNKNOWN_DEVICE_ERROR" } | { currentNumberOfFailedAttempts: number; maxNumberOfFailedAttempts: number; status: "INVALID_TOTP_ERROR" } | { retryAfterMs: number; status: "LIMIT_REACHED_ERROR" }>
    • Parameters

      • tenantId: string
      • userId: string
      • deviceName: string
      • totp: string
      • Optional userContext: Record<string, any>

      Returns Promise<{ status: "OK"; wasAlreadyVerified: boolean } | { status: "UNKNOWN_DEVICE_ERROR" } | { currentNumberOfFailedAttempts: number; maxNumberOfFailedAttempts: number; status: "INVALID_TOTP_ERROR" } | { retryAfterMs: number; status: "LIMIT_REACHED_ERROR" }>

    • verifyTOTP(tenantId: string, userId: string, totp: string, userContext?: Record<string, any>): Promise<{ status: "OK" | "UNKNOWN_USER_ID_ERROR" } | { currentNumberOfFailedAttempts: number; maxNumberOfFailedAttempts: number; status: "INVALID_TOTP_ERROR" } | { retryAfterMs: number; status: "LIMIT_REACHED_ERROR" }>
    • Parameters

      • tenantId: string
      • userId: string
      • totp: string
      • Optional userContext: Record<string, any>

      Returns Promise<{ status: "OK" | "UNKNOWN_USER_ID_ERROR" } | { currentNumberOfFailedAttempts: number; maxNumberOfFailedAttempts: number; status: "INVALID_TOTP_ERROR" } | { retryAfterMs: number; status: "LIMIT_REACHED_ERROR" }>

    Legend

    • Variable
    • Function
    • Function with type parameter
    • Type alias
    • Type alias with type parameter
    • Class
    • Class with type parameter
    • Interface

    Settings

    Theme

    Generated using TypeDoc

    \ No newline at end of file diff --git a/docs/modules/recipe_usermetadata.html b/docs/modules/recipe_usermetadata.html index ca6422bad..47d66b7d6 100644 --- a/docs/modules/recipe_usermetadata.html +++ b/docs/modules/recipe_usermetadata.html @@ -1,4 +1,4 @@ -recipe/usermetadata | supertokens-node
    Options
    All
    • Public
    • Public/Protected
    • All
    Menu

    Module recipe/usermetadata

    Index

    Type Aliases

    RecipeInterface: { clearUserMetadata: any; getUserMetadata: any; updateUserMetadata: any }

    Type declaration

    • clearUserMetadata:function
      • clearUserMetadata(input: { userContext: UserContext; userId: string }): Promise<{ status: "OK" }>
    • getUserMetadata:function
      • getUserMetadata(input: { userContext: UserContext; userId: string }): Promise<{ metadata: any; status: "OK" }>
      • Parameters

        • input: { userContext: UserContext; userId: string }
          • userContext: UserContext
          • userId: string

        Returns Promise<{ metadata: any; status: "OK" }>

    • updateUserMetadata:function
      • updateUserMetadata(input: { metadataUpdate: JSONObject; userContext: UserContext; userId: string }): Promise<{ metadata: JSONObject; status: "OK" }>
      • +recipe/usermetadata | supertokens-node
        Options
        All
        • Public
        • Public/Protected
        • All
        Menu

        Module recipe/usermetadata

        Index

        Type Aliases

        RecipeInterface: { clearUserMetadata: any; getUserMetadata: any; updateUserMetadata: any }

        Type declaration

        • clearUserMetadata:function
          • clearUserMetadata(input: { userContext: UserContext; userId: string }): Promise<{ status: "OK" }>
        • getUserMetadata:function
          • getUserMetadata(input: { userContext: UserContext; userId: string }): Promise<{ metadata: any; status: "OK" }>
          • Parameters

            • input: { userContext: UserContext; userId: string }
              • userContext: UserContext
              • userId: string

            Returns Promise<{ metadata: any; status: "OK" }>

        • updateUserMetadata:function
          • updateUserMetadata(input: { metadataUpdate: JSONObject; userContext: UserContext; userId: string }): Promise<{ metadata: JSONObject; status: "OK" }>
          • Updates the metadata object of the user by doing a shallow merge of the stored and the update JSONs and removing properties set to null on the root level of the update object. e.g.:

            @@ -7,4 +7,4 @@
          • update: { "notifications": { "sms": true }, "todos": null }
          • result: { "preferences": { "theme":"dark" }, "notifications": { "sms": true } }
          -

        Parameters

        • input: { metadataUpdate: JSONObject; userContext: UserContext; userId: string }
          • metadataUpdate: JSONObject
          • userContext: UserContext
          • userId: string

        Returns Promise<{ metadata: JSONObject; status: "OK" }>

    Functions

    • clearUserMetadata(userId: string, userContext?: Record<string, any>): Promise<{ status: "OK" }>
    • getUserMetadata(userId: string, userContext?: Record<string, any>): Promise<{ metadata: any; status: "OK" }>
    • init(config?: TypeInput): RecipeListFunction
    • updateUserMetadata(userId: string, metadataUpdate: JSONObject, userContext?: Record<string, any>): Promise<{ metadata: JSONObject; status: "OK" }>

    Legend

    • Variable
    • Function
    • Function with type parameter
    • Type alias
    • Type alias with type parameter
    • Class
    • Class with type parameter
    • Interface

    Settings

    Theme

    Generated using TypeDoc

    \ No newline at end of file +

    Parameters

    Returns Promise<{ metadata: JSONObject; status: "OK" }>

    Functions

    Generated using TypeDoc

    \ No newline at end of file diff --git a/docs/modules/recipe_userroles.html b/docs/modules/recipe_userroles.html index 109d8a629..cbb8597a1 100644 --- a/docs/modules/recipe_userroles.html +++ b/docs/modules/recipe_userroles.html @@ -1 +1 @@ -recipe/userroles | supertokens-node
    Options
    All
    • Public
    • Public/Protected
    • All
    Menu

    Module recipe/userroles

    Index

    Type Aliases

    RecipeInterface: { addRoleToUser: any; createNewRoleOrAddPermissions: any; deleteRole: any; getAllRoles: any; getPermissionsForRole: any; getRolesForUser: any; getRolesThatHavePermission: any; getUsersThatHaveRole: any; removePermissionsFromRole: any; removeUserRole: any }

    Type declaration

    • addRoleToUser:function
      • addRoleToUser(input: { role: string; tenantId: string; userContext: UserContext; userId: string }): Promise<{ didUserAlreadyHaveRole: boolean; status: "OK" } | { status: "UNKNOWN_ROLE_ERROR" }>
      • Parameters

        • input: { role: string; tenantId: string; userContext: UserContext; userId: string }
          • role: string
          • tenantId: string
          • userContext: UserContext
          • userId: string

        Returns Promise<{ didUserAlreadyHaveRole: boolean; status: "OK" } | { status: "UNKNOWN_ROLE_ERROR" }>

    • createNewRoleOrAddPermissions:function
      • createNewRoleOrAddPermissions(input: { permissions: string[]; role: string; userContext: UserContext }): Promise<{ createdNewRole: boolean; status: "OK" }>
      • Parameters

        • input: { permissions: string[]; role: string; userContext: UserContext }
          • permissions: string[]
          • role: string
          • userContext: UserContext

        Returns Promise<{ createdNewRole: boolean; status: "OK" }>

    • deleteRole:function
      • deleteRole(input: { role: string; userContext: UserContext }): Promise<{ didRoleExist: boolean; status: "OK" }>
      • Parameters

        • input: { role: string; userContext: UserContext }
          • role: string
          • userContext: UserContext

        Returns Promise<{ didRoleExist: boolean; status: "OK" }>

    • getAllRoles:function
      • getAllRoles(input: { userContext: UserContext }): Promise<{ roles: string[]; status: "OK" }>
    • getPermissionsForRole:function
      • getPermissionsForRole(input: { role: string; userContext: UserContext }): Promise<{ permissions: string[]; status: "OK" } | { status: "UNKNOWN_ROLE_ERROR" }>
      • Parameters

        • input: { role: string; userContext: UserContext }
          • role: string
          • userContext: UserContext

        Returns Promise<{ permissions: string[]; status: "OK" } | { status: "UNKNOWN_ROLE_ERROR" }>

    • getRolesForUser:function
      • getRolesForUser(input: { tenantId: string; userContext: UserContext; userId: string }): Promise<{ roles: string[]; status: "OK" }>
      • Parameters

        • input: { tenantId: string; userContext: UserContext; userId: string }
          • tenantId: string
          • userContext: UserContext
          • userId: string

        Returns Promise<{ roles: string[]; status: "OK" }>

    • getRolesThatHavePermission:function
      • getRolesThatHavePermission(input: { permission: string; userContext: UserContext }): Promise<{ roles: string[]; status: "OK" }>
      • Parameters

        • input: { permission: string; userContext: UserContext }
          • permission: string
          • userContext: UserContext

        Returns Promise<{ roles: string[]; status: "OK" }>

    • getUsersThatHaveRole:function
      • getUsersThatHaveRole(input: { role: string; tenantId: string; userContext: UserContext }): Promise<{ status: "OK"; users: string[] } | { status: "UNKNOWN_ROLE_ERROR" }>
      • Parameters

        • input: { role: string; tenantId: string; userContext: UserContext }
          • role: string
          • tenantId: string
          • userContext: UserContext

        Returns Promise<{ status: "OK"; users: string[] } | { status: "UNKNOWN_ROLE_ERROR" }>

    • removePermissionsFromRole:function
      • removePermissionsFromRole(input: { permissions: string[]; role: string; userContext: UserContext }): Promise<{ status: "OK" | "UNKNOWN_ROLE_ERROR" }>
      • Parameters

        • input: { permissions: string[]; role: string; userContext: UserContext }
          • permissions: string[]
          • role: string
          • userContext: UserContext

        Returns Promise<{ status: "OK" | "UNKNOWN_ROLE_ERROR" }>

    • removeUserRole:function
      • removeUserRole(input: { role: string; tenantId: string; userContext: UserContext; userId: string }): Promise<{ didUserHaveRole: boolean; status: "OK" } | { status: "UNKNOWN_ROLE_ERROR" }>
      • Parameters

        • input: { role: string; tenantId: string; userContext: UserContext; userId: string }
          • role: string
          • tenantId: string
          • userContext: UserContext
          • userId: string

        Returns Promise<{ didUserHaveRole: boolean; status: "OK" } | { status: "UNKNOWN_ROLE_ERROR" }>

    Variables

    PermissionClaim: PermissionClaimClass = ...
    UserRoleClaim: UserRoleClaimClass = ...

    Functions

    • addRoleToUser(tenantId: string, userId: string, role: string, userContext?: Record<string, any>): Promise<{ didUserAlreadyHaveRole: boolean; status: "OK" } | { status: "UNKNOWN_ROLE_ERROR" }>
    • Parameters

      • tenantId: string
      • userId: string
      • role: string
      • Optional userContext: Record<string, any>

      Returns Promise<{ didUserAlreadyHaveRole: boolean; status: "OK" } | { status: "UNKNOWN_ROLE_ERROR" }>

    • createNewRoleOrAddPermissions(role: string, permissions: string[], userContext?: Record<string, any>): Promise<{ createdNewRole: boolean; status: "OK" }>
    • Parameters

      • role: string
      • permissions: string[]
      • Optional userContext: Record<string, any>

      Returns Promise<{ createdNewRole: boolean; status: "OK" }>

    • deleteRole(role: string, userContext?: Record<string, any>): Promise<{ didRoleExist: boolean; status: "OK" }>
    • getAllRoles(userContext?: Record<string, any>): Promise<{ roles: string[]; status: "OK" }>
    • getPermissionsForRole(role: string, userContext?: Record<string, any>): Promise<{ permissions: string[]; status: "OK" } | { status: "UNKNOWN_ROLE_ERROR" }>
    • Parameters

      • role: string
      • Optional userContext: Record<string, any>

      Returns Promise<{ permissions: string[]; status: "OK" } | { status: "UNKNOWN_ROLE_ERROR" }>

    • getRolesForUser(tenantId: string, userId: string, userContext?: Record<string, any>): Promise<{ roles: string[]; status: "OK" }>
    • getRolesThatHavePermission(permission: string, userContext?: Record<string, any>): Promise<{ roles: string[]; status: "OK" }>
    • getUsersThatHaveRole(tenantId: string, role: string, userContext?: Record<string, any>): Promise<{ status: "OK"; users: string[] } | { status: "UNKNOWN_ROLE_ERROR" }>
    • Parameters

      • tenantId: string
      • role: string
      • Optional userContext: Record<string, any>

      Returns Promise<{ status: "OK"; users: string[] } | { status: "UNKNOWN_ROLE_ERROR" }>

    • init(config?: TypeInput): RecipeListFunction
    • removePermissionsFromRole(role: string, permissions: string[], userContext?: Record<string, any>): Promise<{ status: "OK" | "UNKNOWN_ROLE_ERROR" }>
    • Parameters

      • role: string
      • permissions: string[]
      • Optional userContext: Record<string, any>

      Returns Promise<{ status: "OK" | "UNKNOWN_ROLE_ERROR" }>

    • removeUserRole(tenantId: string, userId: string, role: string, userContext?: Record<string, any>): Promise<{ didUserHaveRole: boolean; status: "OK" } | { status: "UNKNOWN_ROLE_ERROR" }>
    • Parameters

      • tenantId: string
      • userId: string
      • role: string
      • Optional userContext: Record<string, any>

      Returns Promise<{ didUserHaveRole: boolean; status: "OK" } | { status: "UNKNOWN_ROLE_ERROR" }>

    Generated using TypeDoc

    \ No newline at end of file +recipe/userroles | supertokens-node
    Options
    All
    • Public
    • Public/Protected
    • All
    Menu

    Module recipe/userroles

    Index

    Type Aliases

    RecipeInterface: { addRoleToUser: any; createNewRoleOrAddPermissions: any; deleteRole: any; getAllRoles: any; getPermissionsForRole: any; getRolesForUser: any; getRolesThatHavePermission: any; getUsersThatHaveRole: any; removePermissionsFromRole: any; removeUserRole: any }

    Type declaration

    • addRoleToUser:function
      • addRoleToUser(input: { role: string; tenantId: string; userContext: UserContext; userId: string }): Promise<{ didUserAlreadyHaveRole: boolean; status: "OK" } | { status: "UNKNOWN_ROLE_ERROR" }>
      • Parameters

        • input: { role: string; tenantId: string; userContext: UserContext; userId: string }
          • role: string
          • tenantId: string
          • userContext: UserContext
          • userId: string

        Returns Promise<{ didUserAlreadyHaveRole: boolean; status: "OK" } | { status: "UNKNOWN_ROLE_ERROR" }>

    • createNewRoleOrAddPermissions:function
      • createNewRoleOrAddPermissions(input: { permissions: string[]; role: string; userContext: UserContext }): Promise<{ createdNewRole: boolean; status: "OK" }>
      • Parameters

        • input: { permissions: string[]; role: string; userContext: UserContext }
          • permissions: string[]
          • role: string
          • userContext: UserContext

        Returns Promise<{ createdNewRole: boolean; status: "OK" }>

    • deleteRole:function
      • deleteRole(input: { role: string; userContext: UserContext }): Promise<{ didRoleExist: boolean; status: "OK" }>
      • Parameters

        • input: { role: string; userContext: UserContext }
          • role: string
          • userContext: UserContext

        Returns Promise<{ didRoleExist: boolean; status: "OK" }>

    • getAllRoles:function
      • getAllRoles(input: { userContext: UserContext }): Promise<{ roles: string[]; status: "OK" }>
    • getPermissionsForRole:function
      • getPermissionsForRole(input: { role: string; userContext: UserContext }): Promise<{ permissions: string[]; status: "OK" } | { status: "UNKNOWN_ROLE_ERROR" }>
      • Parameters

        • input: { role: string; userContext: UserContext }
          • role: string
          • userContext: UserContext

        Returns Promise<{ permissions: string[]; status: "OK" } | { status: "UNKNOWN_ROLE_ERROR" }>

    • getRolesForUser:function
      • getRolesForUser(input: { tenantId: string; userContext: UserContext; userId: string }): Promise<{ roles: string[]; status: "OK" }>
      • Parameters

        • input: { tenantId: string; userContext: UserContext; userId: string }
          • tenantId: string
          • userContext: UserContext
          • userId: string

        Returns Promise<{ roles: string[]; status: "OK" }>

    • getRolesThatHavePermission:function
      • getRolesThatHavePermission(input: { permission: string; userContext: UserContext }): Promise<{ roles: string[]; status: "OK" }>
      • Parameters

        • input: { permission: string; userContext: UserContext }
          • permission: string
          • userContext: UserContext

        Returns Promise<{ roles: string[]; status: "OK" }>

    • getUsersThatHaveRole:function
      • getUsersThatHaveRole(input: { role: string; tenantId: string; userContext: UserContext }): Promise<{ status: "OK"; users: string[] } | { status: "UNKNOWN_ROLE_ERROR" }>
      • Parameters

        • input: { role: string; tenantId: string; userContext: UserContext }
          • role: string
          • tenantId: string
          • userContext: UserContext

        Returns Promise<{ status: "OK"; users: string[] } | { status: "UNKNOWN_ROLE_ERROR" }>

    • removePermissionsFromRole:function
      • removePermissionsFromRole(input: { permissions: string[]; role: string; userContext: UserContext }): Promise<{ status: "OK" | "UNKNOWN_ROLE_ERROR" }>
      • Parameters

        • input: { permissions: string[]; role: string; userContext: UserContext }
          • permissions: string[]
          • role: string
          • userContext: UserContext

        Returns Promise<{ status: "OK" | "UNKNOWN_ROLE_ERROR" }>

    • removeUserRole:function
      • removeUserRole(input: { role: string; tenantId: string; userContext: UserContext; userId: string }): Promise<{ didUserHaveRole: boolean; status: "OK" } | { status: "UNKNOWN_ROLE_ERROR" }>
      • Parameters

        • input: { role: string; tenantId: string; userContext: UserContext; userId: string }
          • role: string
          • tenantId: string
          • userContext: UserContext
          • userId: string

        Returns Promise<{ didUserHaveRole: boolean; status: "OK" } | { status: "UNKNOWN_ROLE_ERROR" }>

    Variables

    PermissionClaim: PermissionClaimClass = ...
    UserRoleClaim: UserRoleClaimClass = ...

    Functions

    • addRoleToUser(tenantId: string, userId: string, role: string, userContext?: Record<string, any>): Promise<{ didUserAlreadyHaveRole: boolean; status: "OK" } | { status: "UNKNOWN_ROLE_ERROR" }>
    • Parameters

      • tenantId: string
      • userId: string
      • role: string
      • Optional userContext: Record<string, any>

      Returns Promise<{ didUserAlreadyHaveRole: boolean; status: "OK" } | { status: "UNKNOWN_ROLE_ERROR" }>

    • createNewRoleOrAddPermissions(role: string, permissions: string[], userContext?: Record<string, any>): Promise<{ createdNewRole: boolean; status: "OK" }>
    • Parameters

      • role: string
      • permissions: string[]
      • Optional userContext: Record<string, any>

      Returns Promise<{ createdNewRole: boolean; status: "OK" }>

    • deleteRole(role: string, userContext?: Record<string, any>): Promise<{ didRoleExist: boolean; status: "OK" }>
    • getAllRoles(userContext?: Record<string, any>): Promise<{ roles: string[]; status: "OK" }>
    • getPermissionsForRole(role: string, userContext?: Record<string, any>): Promise<{ permissions: string[]; status: "OK" } | { status: "UNKNOWN_ROLE_ERROR" }>
    • Parameters

      • role: string
      • Optional userContext: Record<string, any>

      Returns Promise<{ permissions: string[]; status: "OK" } | { status: "UNKNOWN_ROLE_ERROR" }>

    • getRolesForUser(tenantId: string, userId: string, userContext?: Record<string, any>): Promise<{ roles: string[]; status: "OK" }>
    • getRolesThatHavePermission(permission: string, userContext?: Record<string, any>): Promise<{ roles: string[]; status: "OK" }>
    • getUsersThatHaveRole(tenantId: string, role: string, userContext?: Record<string, any>): Promise<{ status: "OK"; users: string[] } | { status: "UNKNOWN_ROLE_ERROR" }>
    • Parameters

      • tenantId: string
      • role: string
      • Optional userContext: Record<string, any>

      Returns Promise<{ status: "OK"; users: string[] } | { status: "UNKNOWN_ROLE_ERROR" }>

    • init(config?: TypeInput): RecipeListFunction
    • removePermissionsFromRole(role: string, permissions: string[], userContext?: Record<string, any>): Promise<{ status: "OK" | "UNKNOWN_ROLE_ERROR" }>
    • Parameters

      • role: string
      • permissions: string[]
      • Optional userContext: Record<string, any>

      Returns Promise<{ status: "OK" | "UNKNOWN_ROLE_ERROR" }>

    • removeUserRole(tenantId: string, userId: string, role: string, userContext?: Record<string, any>): Promise<{ didUserHaveRole: boolean; status: "OK" } | { status: "UNKNOWN_ROLE_ERROR" }>
    • Parameters

      • tenantId: string
      • userId: string
      • role: string
      • Optional userContext: Record<string, any>

      Returns Promise<{ didUserHaveRole: boolean; status: "OK" } | { status: "UNKNOWN_ROLE_ERROR" }>

    Generated using TypeDoc

    \ No newline at end of file diff --git a/frontendDriverInterfaceSupported.json b/frontendDriverInterfaceSupported.json index c925842b1..cbdd87f7b 100644 --- a/frontendDriverInterfaceSupported.json +++ b/frontendDriverInterfaceSupported.json @@ -1,4 +1,4 @@ { "_comment": "contains a list of frontend-driver interfaces branch names that this core supports", - "versions": ["1.17", "1.18", "1.19", "2.0", "3.0"] + "versions": ["1.17", "1.18", "1.19", "2.0", "3.0", "3.1"] } diff --git a/lib/build/authUtils.d.ts b/lib/build/authUtils.d.ts index 9e714eb91..98ff4ad41 100644 --- a/lib/build/authUtils.d.ts +++ b/lib/build/authUtils.d.ts @@ -54,6 +54,7 @@ export declare const AuthUtils: { factorIds, skipSessionUserUpdateInCore, session, + shouldTryLinkingWithSessionUser, userContext, }: { authenticatingAccountInfo: AccountInfoWithRecipeId; @@ -65,6 +66,7 @@ export declare const AuthUtils: { signInVerifiesLoginMethod: boolean; skipSessionUserUpdateInCore: boolean; session?: SessionContainerInterface | undefined; + shouldTryLinkingWithSessionUser: boolean | undefined; userContext: UserContext; }) => Promise< | { @@ -194,6 +196,7 @@ export declare const AuthUtils: { */ checkAuthTypeAndLinkingStatus: ( session: SessionContainerInterface | undefined, + shouldTryLinkingWithSessionUser: boolean | undefined, accountInfo: AccountInfoWithRecipeId, inputUser: User | undefined, skipSessionUserUpdateInCore: boolean, @@ -235,17 +238,19 @@ export declare const AuthUtils: { * - LINKING_TO_SESSION_USER_FAILED (SESSION_USER_ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR): * if the session user should be primary but we couldn't make it primary because of a conflicting primary user. */ - linkToSessionIfProvidedElseCreatePrimaryUserIdOrLinkByAccountInfo: ({ + linkToSessionIfRequiredElseCreatePrimaryUserIdOrLinkByAccountInfo: ({ tenantId, inputUser, recipeUserId, session, + shouldTryLinkingWithSessionUser, userContext, }: { tenantId: string; inputUser: User; recipeUserId: RecipeUserId; session: SessionContainerInterface | undefined; + shouldTryLinkingWithSessionUser: boolean | undefined; userContext: UserContext; }) => Promise< | { @@ -335,4 +340,10 @@ export declare const AuthUtils: { hasSession: boolean, userContext: UserContext ) => Promise; + loadSessionInAuthAPIIfNeeded: ( + req: BaseRequest, + res: BaseResponse, + shouldTryLinkingWithSessionUser: boolean | undefined, + userContext: UserContext + ) => Promise; }; diff --git a/lib/build/authUtils.js b/lib/build/authUtils.js index a0afc304a..c340c7930 100644 --- a/lib/build/authUtils.js +++ b/lib/build/authUtils.js @@ -78,6 +78,7 @@ exports.AuthUtils = { factorIds, skipSessionUserUpdateInCore, session, + shouldTryLinkingWithSessionUser, userContext, }) { let validFactorIds; @@ -91,6 +92,7 @@ exports.AuthUtils = { // We also load the session user here if it is available. const authTypeInfo = await exports.AuthUtils.checkAuthTypeAndLinkingStatus( session, + shouldTryLinkingWithSessionUser, authenticatingAccountInfo, authenticatingUser, skipSessionUserUpdateInCore, @@ -255,6 +257,9 @@ exports.AuthUtils = { } } } else { + // We do not have to care about overwriting the session here, since we either: + // - have overwriteSessionDuringSignInUp true and can ignore it + // - have overwriteSessionDuringSignInUp false and we checked in the api imlp that there is no session logger_1.logDebugMessage(`postAuthChecks creating session for first factor sign in/up`); // If there is no input session, we do not need to do anything other checks and create a new session respSession = await session_1.default.createNewSession( @@ -439,6 +444,7 @@ exports.AuthUtils = { */ checkAuthTypeAndLinkingStatus: async function ( session, + shouldTryLinkingWithSessionUser, accountInfo, inputUser, skipSessionUserUpdateInCore, @@ -447,19 +453,37 @@ exports.AuthUtils = { logger_1.logDebugMessage(`checkAuthTypeAndLinkingStatus called`); let sessionUser = undefined; if (session === undefined) { + if (shouldTryLinkingWithSessionUser === true) { + throw new error_1.default({ + type: error_1.default.UNAUTHORISED, + message: "Session not found but shouldTryLinkingWithSessionUser is true", + }); + } logger_1.logDebugMessage( `checkAuthTypeAndLinkingStatus returning first factor because there is no session` ); // If there is no active session we have nothing to link to - so this has to be a first factor sign in return { status: "OK", isFirstFactor: true }; } else { + if (shouldTryLinkingWithSessionUser === false) { + // In our normal flows this should never happen - but some user overrides might do this. + // Anyway, since shouldTryLinkingWithSessionUser explicitly set to false, it's safe to consider this a firstFactor + return { status: "OK", isFirstFactor: true }; + } if (!utils_3.recipeInitDefinedShouldDoAutomaticAccountLinking(recipe_1.default.getInstance().config)) { - if (recipe_2.default.getInstance() !== undefined) { + if (shouldTryLinkingWithSessionUser === true) { throw new Error( "Please initialise the account linking recipe and define shouldDoAutomaticAccountLinking to enable MFA" ); } else { - return { status: "OK", isFirstFactor: true }; + // This is the legacy case where shouldTryLinkingWithSessionUser is undefined + if (recipe_2.default.getInstance() !== undefined) { + throw new Error( + "Please initialise the account linking recipe and define shouldDoAutomaticAccountLinking to enable MFA" + ); + } else { + return { status: "OK", isFirstFactor: true }; + } } } // If the input and the session user are the same @@ -489,6 +513,13 @@ exports.AuthUtils = { userContext ); if (sessionUserResult.status === "SHOULD_AUTOMATICALLY_LINK_FALSE") { + if (shouldTryLinkingWithSessionUser === true) { + throw new _1.Error({ + message: + "shouldDoAutomaticAccountLinking returned false when creating primary user but shouldTryLinkingWithSessionUser is true", + type: "BAD_INPUT_ERROR", + }); + } return { status: "OK", isFirstFactor: true, @@ -519,6 +550,13 @@ exports.AuthUtils = { )}` ); if (shouldLink.shouldAutomaticallyLink === false) { + if (shouldTryLinkingWithSessionUser === true) { + throw new _1.Error({ + message: + "shouldDoAutomaticAccountLinking returned false when creating primary user but shouldTryLinkingWithSessionUser is true", + type: "BAD_INPUT_ERROR", + }); + } return { status: "OK", isFirstFactor: true }; } else { return { @@ -545,20 +583,22 @@ exports.AuthUtils = { * - LINKING_TO_SESSION_USER_FAILED (SESSION_USER_ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR): * if the session user should be primary but we couldn't make it primary because of a conflicting primary user. */ - linkToSessionIfProvidedElseCreatePrimaryUserIdOrLinkByAccountInfo: async function ({ + linkToSessionIfRequiredElseCreatePrimaryUserIdOrLinkByAccountInfo: async function ({ tenantId, inputUser, recipeUserId, session, + shouldTryLinkingWithSessionUser, userContext, }) { logger_1.logDebugMessage("linkToSessionIfProvidedElseCreatePrimaryUserIdOrLinkByAccountInfo called"); const retry = () => { logger_1.logDebugMessage("linkToSessionIfProvidedElseCreatePrimaryUserIdOrLinkByAccountInfo retrying...."); - return exports.AuthUtils.linkToSessionIfProvidedElseCreatePrimaryUserIdOrLinkByAccountInfo({ + return exports.AuthUtils.linkToSessionIfRequiredElseCreatePrimaryUserIdOrLinkByAccountInfo({ tenantId, inputUser: inputUser, session, + shouldTryLinkingWithSessionUser, recipeUserId, userContext, }); @@ -575,6 +615,7 @@ exports.AuthUtils = { } const authTypeRes = await exports.AuthUtils.checkAuthTypeAndLinkingStatus( session, + shouldTryLinkingWithSessionUser, authLoginMethod, inputUser, false, @@ -846,6 +887,21 @@ exports.AuthUtils = { } return validFactorIds; }, + loadSessionInAuthAPIIfNeeded: async function (req, res, shouldTryLinkingWithSessionUser, userContext) { + const overwriteSessionDuringSignInUp = recipe_3.default.getInstanceOrThrowError().config + .overwriteSessionDuringSignInUp; + return shouldTryLinkingWithSessionUser !== false || !overwriteSessionDuringSignInUp + ? await session_1.default.getSession( + req, + res, + { + sessionRequired: shouldTryLinkingWithSessionUser === true, + overrideGlobalClaimValidators: () => [], + }, + userContext + ) + : undefined; + }, }; async function filterOutInvalidSecondFactorsOrThrowIfAllAreInvalid( factorIds, diff --git a/lib/build/combinedRemoteJWKSet.d.ts b/lib/build/combinedRemoteJWKSet.d.ts new file mode 100644 index 000000000..88153ee4e --- /dev/null +++ b/lib/build/combinedRemoteJWKSet.d.ts @@ -0,0 +1,19 @@ +// @ts-nocheck +/** + * We need this to reset the combinedJWKS in tests because we need to create a new instance of the combinedJWKS + * for each test to avoid caching issues. + * This is called when the session recipe is reset and when the oauth2provider recipe is reset. + * Calling this multiple times doesn't cause an issue. + */ +export declare function resetCombinedJWKS(): void; +/** + The function returned by this getter fetches all JWKs from the first available core instance. + This combines the other JWKS functions to become error resistant. + + Every core instance a backend is connected to is expected to connect to the same database and use the same key set for + token verification. Otherwise, the result of session verification would depend on which core is currently available. +*/ +export declare function getCombinedJWKS(): ( + protectedHeader?: import("jose").JWSHeaderParameters | undefined, + token?: import("jose").FlattenedJWSInput | undefined +) => Promise; diff --git a/lib/build/combinedRemoteJWKSet.js b/lib/build/combinedRemoteJWKSet.js new file mode 100644 index 000000000..0adeefc15 --- /dev/null +++ b/lib/build/combinedRemoteJWKSet.js @@ -0,0 +1,69 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.getCombinedJWKS = exports.resetCombinedJWKS = void 0; +const jose_1 = require("jose"); +const constants_1 = require("./recipe/session/constants"); +const querier_1 = require("./querier"); +let combinedJWKS; +/** + * We need this to reset the combinedJWKS in tests because we need to create a new instance of the combinedJWKS + * for each test to avoid caching issues. + * This is called when the session recipe is reset and when the oauth2provider recipe is reset. + * Calling this multiple times doesn't cause an issue. + */ +function resetCombinedJWKS() { + combinedJWKS = undefined; +} +exports.resetCombinedJWKS = resetCombinedJWKS; +// TODO: remove this after proper core support +const hydraJWKS = jose_1.createRemoteJWKSet(new URL("http://localhost:4444/.well-known/jwks.json"), { + cooldownDuration: constants_1.JWKCacheCooldownInMs, +}); +/** + The function returned by this getter fetches all JWKs from the first available core instance. + This combines the other JWKS functions to become error resistant. + + Every core instance a backend is connected to is expected to connect to the same database and use the same key set for + token verification. Otherwise, the result of session verification would depend on which core is currently available. +*/ +function getCombinedJWKS() { + if (combinedJWKS === undefined) { + const JWKS = querier_1.Querier.getNewInstanceOrThrowError(undefined) + .getAllCoreUrlsForPath("/.well-known/jwks.json") + .map((url) => + jose_1.createRemoteJWKSet(new URL(url), { + cooldownDuration: constants_1.JWKCacheCooldownInMs, + }) + ); + combinedJWKS = async (...args) => { + var _a, _b, _c, _d; + let lastError = undefined; + if ( + !((_b = (_a = args[0]) === null || _a === void 0 ? void 0 : _a.kid) === null || _b === void 0 + ? void 0 + : _b.startsWith("s-")) && + !((_d = (_c = args[0]) === null || _c === void 0 ? void 0 : _c.kid) === null || _d === void 0 + ? void 0 + : _d.startsWith("d-")) + ) { + return hydraJWKS(...args); + } + if (JWKS.length === 0) { + throw Error( + "No SuperTokens core available to query. Please pass supertokens > connectionURI to the init function, or override all the functions of the recipe you are using." + ); + } + for (const jwks of JWKS) { + try { + // We await before returning to make sure we catch the error + return await jwks(...args); + } catch (ex) { + lastError = ex; + } + } + throw lastError; + }; + } + return combinedJWKS; +} +exports.getCombinedJWKS = getCombinedJWKS; diff --git a/lib/build/framework/request.d.ts b/lib/build/framework/request.d.ts index fafdcdb20..00c5fe6c3 100644 --- a/lib/build/framework/request.d.ts +++ b/lib/build/framework/request.d.ts @@ -15,4 +15,5 @@ export declare abstract class BaseRequest { abstract getOriginalURL: () => string; getFormData: () => Promise; getJSONBody: () => Promise; + getBodyAsJSONOrFormData: () => Promise; } diff --git a/lib/build/framework/request.js b/lib/build/framework/request.js index afc940e7a..edaa6e780 100644 --- a/lib/build/framework/request.js +++ b/lib/build/framework/request.js @@ -41,6 +41,26 @@ class BaseRequest { } return this.parsedJSONBody; }; + this.getBodyAsJSONOrFormData = async () => { + const contentType = this.getHeaderValue("content-type"); + if (contentType) { + if (contentType.startsWith("application/json")) { + return await this.getJSONBody(); + } else if (contentType.startsWith("application/x-www-form-urlencoded")) { + return await this.getFormData(); + } + } else { + try { + return await this.getJSONBody(); + } catch (_a) { + try { + return await this.getFormData(); + } catch (_b) { + throw new Error("Unable to parse body as JSON or Form Data."); + } + } + } + }; this.wrapperUsed = true; this.parsedJSONBody = undefined; this.parsedUrlEncodedFormData = undefined; diff --git a/lib/build/querier.d.ts b/lib/build/querier.d.ts index dbc232eea..bc25a61fe 100644 --- a/lib/build/querier.d.ts +++ b/lib/build/querier.d.ts @@ -3,6 +3,7 @@ import NormalisedURLDomain from "./normalisedURLDomain"; import NormalisedURLPath from "./normalisedURLPath"; import { UserContext } from "./types"; import { NetworkInterceptor } from "./types"; +export declare const hydraPubDomain: string; export declare class Querier { private static initCalled; private static hosts; @@ -44,12 +45,19 @@ export declare class Querier { sendGetRequestWithResponseHeaders: ( path: NormalisedURLPath, params: Record, + inpHeaders: Record | undefined, userContext: UserContext ) => Promise<{ body: any; headers: Headers; }>; - sendPutRequest: (path: NormalisedURLPath, body: any, userContext: UserContext) => Promise; + sendPutRequest: ( + path: NormalisedURLPath, + body: any, + params: Record, + userContext: UserContext + ) => Promise; + sendPatchRequest: (path: NormalisedURLPath, body: any, userContext: UserContext) => Promise; invalidateCoreCallCache: (userContext: UserContext, updGlobalCacheTagIfNecessary?: boolean) => void; getAllCoreUrlsForPath(path: string): string[]; private sendRequestHelper; diff --git a/lib/build/querier.js b/lib/build/querier.js index e90a3d875..ee366ca91 100644 --- a/lib/build/querier.js +++ b/lib/build/querier.js @@ -4,8 +4,9 @@ var __importDefault = function (mod) { return mod && mod.__esModule ? mod : { default: mod }; }; +var _a, _b; Object.defineProperty(exports, "__esModule", { value: true }); -exports.Querier = void 0; +exports.Querier = exports.hydraPubDomain = void 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 @@ -27,6 +28,10 @@ const processState_1 = require("./processState"); const constants_1 = require("./constants"); const logger_1 = require("./logger"); const supertokens_1 = __importDefault(require("./supertokens")); +exports.hydraPubDomain = (_a = process.env.HYDRA_PUB) !== null && _a !== void 0 ? _a : "http://localhost:4444"; // This will be used as a domain for paths starting with hydraPubPathPrefix +const hydraAdmDomain = (_b = process.env.HYDRA_ADM) !== null && _b !== void 0 ? _b : "http://localhost:4445"; // This will be used as a domain for paths starting with hydraAdmPathPrefix +const hydraPubPathPrefix = "/recipe/oauth2/pub"; // Replaced with "/oauth2" when sending the request (/recipe/oauth2/pub/token -> /oauth2/token) +const hydraAdmPathPrefix = "/recipe/oauth2/admin"; // Replaced with "/admin" when sending the request (/recipe/oauth2/admin/clients -> /admin/clients) class Querier { // we have rIdToCore so that recipes can force change the rId sent to core. This is a hack until the core is able // to support multiple rIds per API @@ -101,6 +106,11 @@ class Querier { this.sendPostRequest = async (path, body, userContext) => { var _a; this.invalidateCoreCallCache(userContext); + // TODO: remove FormData + const isForm = body !== undefined && body["$isFormData"]; + if (isForm) { + delete body["$isFormData"]; + } const { body: respBody } = await this.sendRequestHelper( path, "POST", @@ -108,8 +118,17 @@ class Querier { let apiVersion = await this.getAPIVersion(userContext); let headers = { "cdi-version": apiVersion, - "content-type": "application/json; charset=utf-8", }; + if (isForm) { + headers["content-type"] = "application/x-www-form-urlencoded"; + } else { + headers["content-type"] = "application/json; charset=utf-8"; + } + // TODO: Remove this after core changes are done + if (body !== undefined && body["authorizationHeader"]) { + headers["authorization"] = body["authorizationHeader"]; + delete body["authorizationHeader"]; + } if (Querier.apiKey !== undefined) { headers = Object.assign(Object.assign({}, headers), { "api-key": Querier.apiKey }); } @@ -134,7 +153,11 @@ class Querier { } return utils_1.doFetch(url, { method: "POST", - body: body !== undefined ? JSON.stringify(body) : undefined, + body: isForm + ? new URLSearchParams(Object.entries(body)).toString() + : body !== undefined + ? JSON.stringify(body) + : undefined, headers, }); }, @@ -267,6 +290,9 @@ class Querier { method: "GET", headers, }); + if (response.status === 302) { + return response; + } if (response.status === 200 && !Querier.disableCache) { // If the request was successful, we save the result into the cache // plus we update the cache tag @@ -287,14 +313,15 @@ class Querier { ); return respBody; }; - this.sendGetRequestWithResponseHeaders = async (path, params, userContext) => { + this.sendGetRequestWithResponseHeaders = async (path, params, inpHeaders, userContext) => { var _a; return await this.sendRequestHelper( path, "GET", async (url) => { let apiVersion = await this.getAPIVersion(userContext); - let headers = { "cdi-version": apiVersion }; + let headers = inpHeaders !== null && inpHeaders !== void 0 ? inpHeaders : {}; + headers["cdi-version"] = apiVersion; if (Querier.apiKey !== undefined) { headers = Object.assign(Object.assign({}, headers), { "api-key": Querier.apiKey }); } @@ -331,7 +358,7 @@ class Querier { ); }; // path should start with "/" - this.sendPutRequest = async (path, body, userContext) => { + this.sendPutRequest = async (path, body, params, userContext) => { var _a; this.invalidateCoreCallCache(userContext); const { body: respBody } = await this.sendRequestHelper( @@ -353,6 +380,7 @@ class Querier { method: "put", headers: headers, body: body, + params: params, }, userContext ); @@ -362,7 +390,12 @@ class Querier { body = request.body; } } - return utils_1.doFetch(url, { + const finalURL = new URL(url); + const searchParams = new URLSearchParams( + Object.entries(params).filter(([_, value]) => value !== undefined) + ); + finalURL.search = searchParams.toString(); + return utils_1.doFetch(finalURL.toString(), { method: "PUT", body: body !== undefined ? JSON.stringify(body) : undefined, headers, @@ -372,6 +405,48 @@ class Querier { ); return respBody; }; + // path should start with "/" + this.sendPatchRequest = async (path, body, userContext) => { + var _a; + this.invalidateCoreCallCache(userContext); + const { body: respBody } = await this.sendRequestHelper( + path, + "PATCH", + async (url) => { + let apiVersion = await this.getAPIVersion(userContext); + let headers = { "cdi-version": apiVersion, "content-type": "application/json; charset=utf-8" }; + if (Querier.apiKey !== undefined) { + headers = Object.assign(Object.assign({}, headers), { "api-key": Querier.apiKey }); + } + if (path.isARecipePath() && this.rIdToCore !== undefined) { + headers = Object.assign(Object.assign({}, headers), { rid: this.rIdToCore }); + } + if (Querier.networkInterceptor !== undefined) { + let request = Querier.networkInterceptor( + { + url: url, + method: "patch", + headers: headers, + body: body, + }, + userContext + ); + url = request.url; + headers = request.headers; + if (request.body !== undefined) { + body = request.body; + } + } + return utils_1.doFetch(url, { + method: "PATCH", + body: body !== undefined ? JSON.stringify(body) : undefined, + headers, + }); + }, + ((_a = this.__hosts) === null || _a === void 0 ? void 0 : _a.length) || 0 + ); + return respBody; + }; this.invalidateCoreCallCache = (userContext, updGlobalCacheTagIfNecessary = true) => { var _a; if ( @@ -395,7 +470,19 @@ class Querier { } let currentDomain = this.__hosts[Querier.lastTriedIndex].domain.getAsStringDangerous(); let currentBasePath = this.__hosts[Querier.lastTriedIndex].basePath.getAsStringDangerous(); - const url = currentDomain + currentBasePath + path.getAsStringDangerous(); + let strPath = path.getAsStringDangerous(); + const isHydraAPICall = strPath.startsWith(hydraAdmPathPrefix) || strPath.startsWith(hydraPubPathPrefix); + if (strPath.startsWith(hydraPubPathPrefix)) { + currentDomain = exports.hydraPubDomain; + currentBasePath = ""; + strPath = strPath.replace(hydraPubPathPrefix, "/oauth2"); + } + if (strPath.startsWith(hydraAdmPathPrefix)) { + currentDomain = hydraAdmDomain; + currentBasePath = ""; + strPath = strPath.replace(hydraAdmPathPrefix, "/admin"); + } + const url = currentDomain + currentBasePath + strPath; const maxRetries = 5; if (retryInfoMap === undefined) { retryInfoMap = {}; @@ -414,6 +501,10 @@ class Querier { if (process.env.TEST_MODE === "testing") { Querier.hostsAliveForTesting.add(currentDomain + currentBasePath); } + // TODO: Temporary solution for handling Hydra API calls. Remove when Hydra is no longer called directly. + if (isHydraAPICall) { + return handleHydraAPICall(response); + } if (response.status !== 200) { throw response; } @@ -511,3 +602,26 @@ Querier.hostsAliveForTesting = new Set(); Querier.networkInterceptor = undefined; Querier.globalCacheTag = Date.now(); Querier.disableCache = false; +async function handleHydraAPICall(response) { + const contentType = response.headers.get("Content-Type"); + if (contentType === null || contentType === void 0 ? void 0 : contentType.startsWith("application/json")) { + return { + body: { + status: response.ok ? "OK" : "ERROR", + statusCode: response.status, + data: await response.clone().json(), + }, + headers: response.headers, + }; + } else if (contentType === null || contentType === void 0 ? void 0 : contentType.startsWith("text/plain")) { + return { + body: { + status: response.ok ? "OK" : "ERROR", + statusCode: response.status, + data: await response.clone().text(), + }, + headers: response.headers, + }; + } + return { body: { status: response.ok ? "OK" : "ERROR", statusCode: response.status }, headers: response.headers }; +} diff --git a/lib/build/recipe/accountlinking/index.js b/lib/build/recipe/accountlinking/index.js index 68e7fbbb5..f65eeeaee 100644 --- a/lib/build/recipe/accountlinking/index.js +++ b/lib/build/recipe/accountlinking/index.js @@ -129,6 +129,9 @@ class Wrapper { } static async isEmailChangeAllowed(recipeUserId, newEmail, isVerified, session, userContext) { const user = await __1.getUser(recipeUserId.getAsString(), userContext); + if (user === undefined) { + throw new Error("Passed in recipe user id does not exist"); + } const res = await recipe_1.default.getInstance().isEmailChangeAllowed({ user, newEmail, diff --git a/lib/build/recipe/accountlinking/recipe.d.ts b/lib/build/recipe/accountlinking/recipe.d.ts index 0bf9c36d0..5a850c2a0 100644 --- a/lib/build/recipe/accountlinking/recipe.d.ts +++ b/lib/build/recipe/accountlinking/recipe.d.ts @@ -99,7 +99,7 @@ export default class Recipe extends RecipeModule { userContext: UserContext; }) => Promise; isEmailChangeAllowed: (input: { - user?: User; + user: User; newEmail: string; isVerified: boolean; session: SessionContainerInterface | undefined; diff --git a/lib/build/recipe/accountlinking/recipe.js b/lib/build/recipe/accountlinking/recipe.js index 0e29dd06f..e9eeeb363 100644 --- a/lib/build/recipe/accountlinking/recipe.js +++ b/lib/build/recipe/accountlinking/recipe.js @@ -385,9 +385,6 @@ class Recipe extends recipeModule_1.default { * in account take over if this recipe user is malicious. */ let inputUser = input.user; - if (inputUser === undefined) { - throw new Error("Passed in recipe user id does not exist"); - } for (const tenantId of inputUser.tenantIds) { let existingUsersWithNewEmail = await this.recipeInterfaceImpl.listUsersByAccountInfo({ tenantId, @@ -800,7 +797,7 @@ class Recipe extends recipeModule_1.default { // we can use the 0 index cause targetUser is not a primary user. let shouldDoAccountLinking = await this.config.shouldDoAutomaticAccountLinking( inputUser.loginMethods[0], - primaryUserThatCanBeLinkedToTheInputUser, + createPrimaryUserResult.user, session, tenantId, userContext diff --git a/lib/build/recipe/dashboard/api/multitenancy/createOrUpdateThirdPartyConfig.js b/lib/build/recipe/dashboard/api/multitenancy/createOrUpdateThirdPartyConfig.js index 0369d066d..7586e9f9b 100644 --- a/lib/build/recipe/dashboard/api/multitenancy/createOrUpdateThirdPartyConfig.js +++ b/lib/build/recipe/dashboard/api/multitenancy/createOrUpdateThirdPartyConfig.js @@ -9,7 +9,7 @@ const multitenancy_1 = __importDefault(require("../../../multitenancy")); const recipe_1 = __importDefault(require("../../../multitenancy/recipe")); const normalisedURLDomain_1 = __importDefault(require("../../../../normalisedURLDomain")); const normalisedURLPath_1 = __importDefault(require("../../../../normalisedURLPath")); -const utils_1 = require("../../../thirdparty/providers/utils"); +const thirdpartyUtils_1 = require("../../../../thirdpartyUtils"); async function createOrUpdateThirdPartyConfig(_, tenantId, options, userContext) { var _a; const requestBody = await options.req.getJSONBody(); @@ -71,7 +71,7 @@ async function createOrUpdateThirdPartyConfig(_, tenantId, options, userContext) const normalisedDomain = new normalisedURLDomain_1.default(boxyURL); const normalisedBasePath = new normalisedURLPath_1.default(boxyURL); const connectionsPath = new normalisedURLPath_1.default("/api/v1/saml/config"); - const resp = await utils_1.doPostRequest( + const resp = await thirdpartyUtils_1.doPostRequest( normalisedDomain.getAsStringDangerous() + normalisedBasePath.appendPath(connectionsPath).getAsStringDangerous(), requestBody, diff --git a/lib/build/recipe/dashboard/api/multitenancy/getThirdPartyConfig.js b/lib/build/recipe/dashboard/api/multitenancy/getThirdPartyConfig.js index f5b6c69d5..91278ce5f 100644 --- a/lib/build/recipe/dashboard/api/multitenancy/getThirdPartyConfig.js +++ b/lib/build/recipe/dashboard/api/multitenancy/getThirdPartyConfig.js @@ -21,7 +21,7 @@ const recipe_1 = __importDefault(require("../../../multitenancy/recipe")); const configUtils_1 = require("../../../thirdparty/providers/configUtils"); const normalisedURLDomain_1 = __importDefault(require("../../../../normalisedURLDomain")); const normalisedURLPath_1 = __importDefault(require("../../../../normalisedURLPath")); -const utils_1 = require("../../../thirdparty/providers/utils"); +const thirdpartyUtils_1 = require("../../../../thirdpartyUtils"); async function getThirdPartyConfig(_, tenantId, options, userContext) { var _a, _b, _c, _d, _e; let tenantRes = await multitenancy_1.default.getTenant(tenantId, userContext); @@ -246,7 +246,7 @@ async function getThirdPartyConfig(_, tenantId, options, userContext) { const normalisedDomain = new normalisedURLDomain_1.default(boxyURL); const normalisedBasePath = new normalisedURLPath_1.default(boxyURL); const connectionsPath = new normalisedURLPath_1.default("/api/v1/saml/config"); - const resp = await utils_1.doGetRequest( + const resp = await thirdpartyUtils_1.doGetRequest( normalisedDomain.getAsStringDangerous() + normalisedBasePath.appendPath(connectionsPath).getAsStringDangerous(), { diff --git a/lib/build/recipe/emailpassword/api/implementation.js b/lib/build/recipe/emailpassword/api/implementation.js index 6ed81771b..8e5e65db9 100644 --- a/lib/build/recipe/emailpassword/api/implementation.js +++ b/lib/build/recipe/emailpassword/api/implementation.js @@ -481,7 +481,14 @@ function getAPIImplementation() { ); } }, - signInPOST: async function ({ formFields, tenantId, session, options, userContext }) { + signInPOST: async function ({ + formFields, + tenantId, + session, + shouldTryLinkingWithSessionUser, + options, + userContext, + }) { const errorCodeMap = { SIGN_IN_NOT_ALLOWED: "Cannot sign in due to security reasons. Please try resetting your password, use a different login method or contact support. (ERR_CODE_008)", @@ -546,6 +553,7 @@ function getAPIImplementation() { tenantId, userContext, session, + shouldTryLinkingWithSessionUser, }); if (preAuthChecks.status === "SIGN_UP_NOT_ALLOWED") { throw new Error("This should never happen: pre-auth checks should not fail for sign in"); @@ -567,6 +575,7 @@ function getAPIImplementation() { email, password, session, + shouldTryLinkingWithSessionUser, tenantId, userContext, }); @@ -604,7 +613,14 @@ function getAPIImplementation() { user: postAuthChecks.user, }; }, - signUpPOST: async function ({ formFields, tenantId, session, options, userContext }) { + signUpPOST: async function ({ + formFields, + tenantId, + session, + shouldTryLinkingWithSessionUser, + options, + userContext, + }) { const errorCodeMap = { SIGN_UP_NOT_ALLOWED: "Cannot sign up due to security reasons. Please try logging in, use a different login method or contact support. (ERR_CODE_007)", @@ -635,6 +651,7 @@ function getAPIImplementation() { tenantId, userContext, session, + shouldTryLinkingWithSessionUser, }); if (preAuthCheckRes.status === "SIGN_UP_NOT_ALLOWED") { const conflictingUsers = await recipe_1.default @@ -675,6 +692,7 @@ function getAPIImplementation() { email, password, session, + shouldTryLinkingWithSessionUser, userContext, }); if (signUpResponse.status === "EMAIL_ALREADY_EXISTS_ERROR") { diff --git a/lib/build/recipe/emailpassword/api/signin.js b/lib/build/recipe/emailpassword/api/signin.js index 0ddb015d1..42a7f16b3 100644 --- a/lib/build/recipe/emailpassword/api/signin.js +++ b/lib/build/recipe/emailpassword/api/signin.js @@ -13,34 +13,28 @@ * 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 utils_1 = require("../../../utils"); const utils_2 = require("./utils"); -const session_1 = __importDefault(require("../../session")); +const authUtils_1 = require("../../../authUtils"); async function signInAPI(apiImplementation, tenantId, options, userContext) { // Logic as per https://github.com/supertokens/supertokens-node/issues/20#issuecomment-710346362 if (apiImplementation.signInPOST === undefined) { return false; } + const body = await options.req.getJSONBody(); // step 1 let formFields = await utils_2.validateFormFieldsOrThrowError( options.config.signInFeature.formFields, - (await options.req.getJSONBody()).formFields, + body.formFields, tenantId, userContext ); - let session = await session_1.default.getSession( + const shouldTryLinkingWithSessionUser = utils_1.getNormalisedShouldTryLinkingWithSessionUserFlag(options.req, body); + const session = await authUtils_1.AuthUtils.loadSessionInAuthAPIIfNeeded( options.req, options.res, - { - sessionRequired: false, - overrideGlobalClaimValidators: () => [], - }, + shouldTryLinkingWithSessionUser, userContext ); if (session !== undefined) { @@ -50,6 +44,7 @@ async function signInAPI(apiImplementation, tenantId, options, userContext) { formFields, tenantId, session, + shouldTryLinkingWithSessionUser, options, userContext, }); diff --git a/lib/build/recipe/emailpassword/api/signup.js b/lib/build/recipe/emailpassword/api/signup.js index 0987b15fb..6a793ec2c 100644 --- a/lib/build/recipe/emailpassword/api/signup.js +++ b/lib/build/recipe/emailpassword/api/signup.js @@ -22,7 +22,7 @@ Object.defineProperty(exports, "__esModule", { value: true }); const utils_1 = require("../../../utils"); const utils_2 = require("./utils"); const error_1 = __importDefault(require("../error")); -const session_1 = __importDefault(require("../../session")); +const authUtils_1 = require("../../../authUtils"); async function signUpAPI(apiImplementation, tenantId, options, userContext) { // Logic as per https://github.com/supertokens/supertokens-node/issues/21#issuecomment-710423536 if (apiImplementation.signUpPOST === undefined) { @@ -36,13 +36,14 @@ async function signUpAPI(apiImplementation, tenantId, options, userContext) { tenantId, userContext ); - let session = await session_1.default.getSession( + const shouldTryLinkingWithSessionUser = utils_1.getNormalisedShouldTryLinkingWithSessionUserFlag( + options.req, + requestBody + ); + const session = await authUtils_1.AuthUtils.loadSessionInAuthAPIIfNeeded( options.req, options.res, - { - sessionRequired: false, - overrideGlobalClaimValidators: () => [], - }, + shouldTryLinkingWithSessionUser, userContext ); if (session !== undefined) { @@ -52,6 +53,7 @@ async function signUpAPI(apiImplementation, tenantId, options, userContext) { formFields, tenantId, session, + shouldTryLinkingWithSessionUser, options, userContext: userContext, }); diff --git a/lib/build/recipe/emailpassword/index.js b/lib/build/recipe/emailpassword/index.js index 7fcbacf41..2c77224c0 100644 --- a/lib/build/recipe/emailpassword/index.js +++ b/lib/build/recipe/emailpassword/index.js @@ -33,6 +33,7 @@ class Wrapper { email, password, session, + shouldTryLinkingWithSessionUser: !!session, tenantId: tenantId === undefined ? constants_1.DEFAULT_TENANT_ID : tenantId, userContext: utils_2.getUserContext(userContext), }); @@ -42,6 +43,7 @@ class Wrapper { email, password, session, + shouldTryLinkingWithSessionUser: !!session, tenantId: tenantId === undefined ? constants_1.DEFAULT_TENANT_ID : tenantId, userContext: utils_2.getUserContext(userContext), }); diff --git a/lib/build/recipe/emailpassword/recipeImplementation.js b/lib/build/recipe/emailpassword/recipeImplementation.js index 8c09609c1..acbbb1c7f 100644 --- a/lib/build/recipe/emailpassword/recipeImplementation.js +++ b/lib/build/recipe/emailpassword/recipeImplementation.js @@ -16,7 +16,7 @@ const user_1 = require("../../user"); const authUtils_1 = require("../../authUtils"); function getRecipeInterface(querier, getEmailPasswordConfig) { return { - signUp: async function ({ email, password, tenantId, session, userContext }) { + signUp: async function ({ email, password, tenantId, session, shouldTryLinkingWithSessionUser, userContext }) { const response = await this.createNewRecipeUser({ email, password, @@ -27,12 +27,13 @@ function getRecipeInterface(querier, getEmailPasswordConfig) { return response; } let updatedUser = response.user; - const linkResult = await authUtils_1.AuthUtils.linkToSessionIfProvidedElseCreatePrimaryUserIdOrLinkByAccountInfo( + const linkResult = await authUtils_1.AuthUtils.linkToSessionIfRequiredElseCreatePrimaryUserIdOrLinkByAccountInfo( { tenantId, inputUser: response.user, recipeUserId: response.recipeUserId, session, + shouldTryLinkingWithSessionUser, userContext, } ); @@ -68,7 +69,7 @@ function getRecipeInterface(querier, getEmailPasswordConfig) { // we do not do email verification here cause it's a new user and email password // users are always initially unverified. }, - signIn: async function ({ email, password, tenantId, session, userContext }) { + signIn: async function ({ email, password, tenantId, session, shouldTryLinkingWithSessionUser, userContext }) { const response = await this.verifyCredentials({ email, password, tenantId, userContext }); if (response.status === "OK") { const loginMethod = response.user.loginMethods.find( @@ -93,12 +94,13 @@ function getRecipeInterface(querier, getEmailPasswordConfig) { // function updated the verification status) and can return that response.user = await __1.getUser(response.recipeUserId.getAsString(), userContext); } - const linkResult = await authUtils_1.AuthUtils.linkToSessionIfProvidedElseCreatePrimaryUserIdOrLinkByAccountInfo( + const linkResult = await authUtils_1.AuthUtils.linkToSessionIfRequiredElseCreatePrimaryUserIdOrLinkByAccountInfo( { tenantId, inputUser: response.user, recipeUserId: response.recipeUserId, session, + shouldTryLinkingWithSessionUser, userContext, } ); @@ -223,6 +225,7 @@ function getRecipeInterface(querier, getEmailPasswordConfig) { email: input.email, password: input.password, }, + {}, input.userContext ); if (response.status === "OK") { diff --git a/lib/build/recipe/emailpassword/types.d.ts b/lib/build/recipe/emailpassword/types.d.ts index 58be90676..ee247490e 100644 --- a/lib/build/recipe/emailpassword/types.d.ts +++ b/lib/build/recipe/emailpassword/types.d.ts @@ -67,6 +67,7 @@ export declare type RecipeInterface = { email: string; password: string; session: SessionContainerInterface | undefined; + shouldTryLinkingWithSessionUser: boolean | undefined; tenantId: string; userContext: UserContext; }): Promise< @@ -106,6 +107,7 @@ export declare type RecipeInterface = { email: string; password: string; session: SessionContainerInterface | undefined; + shouldTryLinkingWithSessionUser: boolean | undefined; tenantId: string; userContext: UserContext; }): Promise< @@ -275,6 +277,7 @@ export declare type APIInterface = { }[]; tenantId: string; session: SessionContainerInterface | undefined; + shouldTryLinkingWithSessionUser: boolean | undefined; options: APIOptions; userContext: UserContext; }) => Promise< @@ -301,6 +304,7 @@ export declare type APIInterface = { }[]; tenantId: string; session: SessionContainerInterface | undefined; + shouldTryLinkingWithSessionUser: boolean | undefined; options: APIOptions; userContext: UserContext; }) => Promise< diff --git a/lib/build/recipe/jwt/api/implementation.js b/lib/build/recipe/jwt/api/implementation.js index e52174f38..cbef7fd16 100644 --- a/lib/build/recipe/jwt/api/implementation.js +++ b/lib/build/recipe/jwt/api/implementation.js @@ -21,6 +21,15 @@ function getAPIImplementation() { if (resp.validityInSeconds !== undefined) { options.res.setHeader("Cache-Control", `max-age=${resp.validityInSeconds}, must-revalidate`, false); } + const oauth2Provider = require("../../oauth2provider/recipe").default.getInstance(); + // TODO: dirty hack until we get core support + if (oauth2Provider !== undefined) { + const oauth2JWKSRes = await fetch("http://localhost:4444/.well-known/jwks.json"); + if (oauth2JWKSRes.ok) { + const oauth2RespBody = await oauth2JWKSRes.json(); + resp.keys = resp.keys.concat(oauth2RespBody.keys); + } + } return { keys: resp.keys, }; diff --git a/lib/build/recipe/jwt/recipeImplementation.js b/lib/build/recipe/jwt/recipeImplementation.js index 073f14b88..0ac8ddf6f 100644 --- a/lib/build/recipe/jwt/recipeImplementation.js +++ b/lib/build/recipe/jwt/recipeImplementation.js @@ -54,6 +54,7 @@ function getRecipeInterface(querier, config, appInfo) { const { body, headers } = await querier.sendGetRequestWithResponseHeaders( new normalisedURLPath_1.default("/.well-known/jwks.json"), {}, + undefined, userContext ); let validityInSeconds = defaultJWKSMaxAge; diff --git a/lib/build/recipe/multifactorauth/multiFactorAuthClaim.d.ts b/lib/build/recipe/multifactorauth/multiFactorAuthClaim.d.ts index 9952b0124..07976ada7 100644 --- a/lib/build/recipe/multifactorauth/multiFactorAuthClaim.d.ts +++ b/lib/build/recipe/multifactorauth/multiFactorAuthClaim.d.ts @@ -55,8 +55,10 @@ export declare class MultiFactorAuthClaimClass extends SessionClaim { [x: string]: import("../../types").JSONValue; }; - removeFromPayloadByMerge_internal: () => { - [x: string]: null; + removeFromPayloadByMerge_internal: ( + payload: JSONObject + ) => { + [x: string]: import("../../types").JSONValue; }; getValueFromPayload: (payload: JSONObject) => MFAClaimValue; } diff --git a/lib/build/recipe/multifactorauth/multiFactorAuthClaim.js b/lib/build/recipe/multifactorauth/multiFactorAuthClaim.js index 45fe7885c..da46bd6b1 100644 --- a/lib/build/recipe/multifactorauth/multiFactorAuthClaim.js +++ b/lib/build/recipe/multifactorauth/multiFactorAuthClaim.js @@ -53,10 +53,8 @@ class MultiFactorAuthClaimClass extends claims_1.SessionClaim { delete retVal[this.key]; return retVal; }; - this.removeFromPayloadByMerge_internal = () => { - return { - [this.key]: null, - }; + this.removeFromPayloadByMerge_internal = (payload) => { + return Object.assign(Object.assign({}, payload), { [this.key]: null }); }; this.getValueFromPayload = (payload) => { return payload[this.key]; diff --git a/lib/build/recipe/multitenancy/recipeImplementation.js b/lib/build/recipe/multitenancy/recipeImplementation.js index 2ccd42009..044c7e319 100644 --- a/lib/build/recipe/multitenancy/recipeImplementation.js +++ b/lib/build/recipe/multitenancy/recipeImplementation.js @@ -16,6 +16,7 @@ function getRecipeInterface(querier) { let response = await querier.sendPutRequest( new normalisedURLPath_1.default(`/recipe/multitenancy/tenant/v2`), Object.assign({ tenantId }, config), + {}, userContext ); return response; @@ -64,6 +65,7 @@ function getRecipeInterface(querier) { config, skipValidation, }, + {}, userContext ); return response; diff --git a/lib/build/recipe/oauth2client/api/implementation.d.ts b/lib/build/recipe/oauth2client/api/implementation.d.ts new file mode 100644 index 000000000..dd40e7025 --- /dev/null +++ b/lib/build/recipe/oauth2client/api/implementation.d.ts @@ -0,0 +1,3 @@ +// @ts-nocheck +import { APIInterface } from "../"; +export default function getAPIInterface(): APIInterface; diff --git a/lib/build/recipe/oauth2client/api/implementation.js b/lib/build/recipe/oauth2client/api/implementation.js new file mode 100644 index 000000000..159016c57 --- /dev/null +++ b/lib/build/recipe/oauth2client/api/implementation.js @@ -0,0 +1,57 @@ +"use strict"; +var __importDefault = + (this && this.__importDefault) || + function (mod) { + return mod && mod.__esModule ? mod : { default: mod }; + }; +Object.defineProperty(exports, "__esModule", { value: true }); +const session_1 = __importDefault(require("../../session")); +function getAPIInterface() { + return { + signInPOST: async function (input) { + const { options, tenantId, userContext } = input; + const providerConfig = await options.recipeImplementation.getProviderConfig({ userContext }); + let oAuthTokensToUse = {}; + if ("redirectURIInfo" in input && input.redirectURIInfo !== undefined) { + oAuthTokensToUse = await options.recipeImplementation.exchangeAuthCodeForOAuthTokens({ + providerConfig, + redirectURIInfo: input.redirectURIInfo, + userContext, + }); + } else if ("oAuthTokens" in input && input.oAuthTokens !== undefined) { + oAuthTokensToUse = input.oAuthTokens; + } else { + throw Error("should never come here"); + } + const { userId, rawUserInfo } = await options.recipeImplementation.getUserInfo({ + providerConfig, + oAuthTokens: oAuthTokensToUse, + userContext, + }); + const { user, recipeUserId } = await options.recipeImplementation.signIn({ + userId, + tenantId, + rawUserInfo, + oAuthTokens: oAuthTokensToUse, + userContext, + }); + const session = await session_1.default.createNewSession( + options.req, + options.res, + tenantId, + recipeUserId, + undefined, + undefined, + userContext + ); + return { + status: "OK", + user, + session, + oAuthTokens: oAuthTokensToUse, + rawUserInfo, + }; + }, + }; +} +exports.default = getAPIInterface; diff --git a/lib/build/recipe/oauth2client/api/signin.d.ts b/lib/build/recipe/oauth2client/api/signin.d.ts new file mode 100644 index 000000000..72cd6e46b --- /dev/null +++ b/lib/build/recipe/oauth2client/api/signin.d.ts @@ -0,0 +1,9 @@ +// @ts-nocheck +import { APIInterface, APIOptions } from ".."; +import { UserContext } from "../../../types"; +export default function signInAPI( + apiImplementation: APIInterface, + tenantId: string, + options: APIOptions, + userContext: UserContext +): Promise; diff --git a/lib/build/recipe/oauth2client/api/signin.js b/lib/build/recipe/oauth2client/api/signin.js new file mode 100644 index 000000000..52631ac60 --- /dev/null +++ b/lib/build/recipe/oauth2client/api/signin.js @@ -0,0 +1,80 @@ +"use strict"; +/* Copyright (c) 2024, 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")); +const utils_1 = require("../../../utils"); +const session_1 = __importDefault(require("../../session")); +async function signInAPI(apiImplementation, tenantId, options, userContext) { + if (apiImplementation.signInPOST === undefined) { + return false; + } + const bodyParams = await options.req.getJSONBody(); + let redirectURIInfo; + let oAuthTokens; + if (bodyParams.redirectURIInfo !== undefined) { + if (bodyParams.redirectURIInfo.redirectURI === undefined) { + throw new error_1.default({ + type: error_1.default.BAD_INPUT_ERROR, + message: "Please provide the redirectURI in request body", + }); + } + redirectURIInfo = bodyParams.redirectURIInfo; + } else if (bodyParams.oAuthTokens !== undefined) { + oAuthTokens = bodyParams.oAuthTokens; + } else { + throw new error_1.default({ + type: error_1.default.BAD_INPUT_ERROR, + message: "Please provide one of redirectURIInfo or oAuthTokens in the request body", + }); + } + let session = await session_1.default.getSession( + options.req, + options.res, + { + sessionRequired: false, + overrideGlobalClaimValidators: () => [], + }, + userContext + ); + if (session !== undefined) { + tenantId = session.getTenantId(); + } + let result = await apiImplementation.signInPOST({ + tenantId, + redirectURIInfo, + oAuthTokens, + options, + userContext, + }); + if (result.status === "OK") { + utils_1.send200Response( + options.res, + Object.assign( + { status: result.status }, + utils_1.getBackwardsCompatibleUserInfo(options.req, result, userContext) + ) + ); + } else { + utils_1.send200Response(options.res, result); + } + return true; +} +exports.default = signInAPI; diff --git a/lib/build/recipe/oauth2client/constants.d.ts b/lib/build/recipe/oauth2client/constants.d.ts new file mode 100644 index 000000000..1fb91e760 --- /dev/null +++ b/lib/build/recipe/oauth2client/constants.d.ts @@ -0,0 +1,2 @@ +// @ts-nocheck +export declare const SIGN_IN_API = "/oauth/client/signin"; diff --git a/lib/build/recipe/oauth2client/constants.js b/lib/build/recipe/oauth2client/constants.js new file mode 100644 index 000000000..4f42a6cb4 --- /dev/null +++ b/lib/build/recipe/oauth2client/constants.js @@ -0,0 +1,18 @@ +"use strict"; +/* Copyright (c) 2024, 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. + */ +Object.defineProperty(exports, "__esModule", { value: true }); +exports.SIGN_IN_API = void 0; +exports.SIGN_IN_API = "/oauth/client/signin"; diff --git a/lib/build/recipe/oauth2client/index.d.ts b/lib/build/recipe/oauth2client/index.d.ts new file mode 100644 index 000000000..fd09feb41 --- /dev/null +++ b/lib/build/recipe/oauth2client/index.d.ts @@ -0,0 +1,22 @@ +// @ts-nocheck +import Recipe from "./recipe"; +import { RecipeInterface, APIInterface, APIOptions, OAuthTokens } from "./types"; +export default class Wrapper { + static init: typeof Recipe.init; + static exchangeAuthCodeForOAuthTokens( + redirectURIInfo: { + redirectURI: string; + redirectURIQueryParams: any; + pkceCodeVerifier?: string | undefined; + }, + userContext?: Record + ): Promise; + static getUserInfo( + oAuthTokens: OAuthTokens, + userContext?: Record + ): Promise; +} +export declare let init: typeof Recipe.init; +export declare let exchangeAuthCodeForOAuthTokens: typeof Wrapper.exchangeAuthCodeForOAuthTokens; +export declare let getUserInfo: typeof Wrapper.getUserInfo; +export type { RecipeInterface, APIInterface, APIOptions }; diff --git a/lib/build/recipe/oauth2client/index.js b/lib/build/recipe/oauth2client/index.js new file mode 100644 index 000000000..c70a69f41 --- /dev/null +++ b/lib/build/recipe/oauth2client/index.js @@ -0,0 +1,55 @@ +"use strict"; +/* Copyright (c) 2024, 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 }); +exports.getUserInfo = exports.exchangeAuthCodeForOAuthTokens = exports.init = void 0; +const utils_1 = require("../../utils"); +const recipe_1 = __importDefault(require("./recipe")); +class Wrapper { + static async exchangeAuthCodeForOAuthTokens(redirectURIInfo, userContext) { + const recipeInterfaceImpl = recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl; + const normalisedUserContext = utils_1.getUserContext(userContext); + const providerConfig = await recipeInterfaceImpl.getProviderConfig({ + userContext: normalisedUserContext, + }); + return await recipeInterfaceImpl.exchangeAuthCodeForOAuthTokens({ + providerConfig, + redirectURIInfo, + userContext: normalisedUserContext, + }); + } + static async getUserInfo(oAuthTokens, userContext) { + const recipeInterfaceImpl = recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl; + const normalisedUserContext = utils_1.getUserContext(userContext); + const providerConfig = await recipeInterfaceImpl.getProviderConfig({ + userContext: normalisedUserContext, + }); + return await recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.getUserInfo({ + providerConfig, + oAuthTokens, + userContext: normalisedUserContext, + }); + } +} +exports.default = Wrapper; +Wrapper.init = recipe_1.default.init; +exports.init = Wrapper.init; +exports.exchangeAuthCodeForOAuthTokens = Wrapper.exchangeAuthCodeForOAuthTokens; +exports.getUserInfo = Wrapper.getUserInfo; diff --git a/lib/build/recipe/oauth2client/recipe.d.ts b/lib/build/recipe/oauth2client/recipe.d.ts new file mode 100644 index 000000000..180227169 --- /dev/null +++ b/lib/build/recipe/oauth2client/recipe.d.ts @@ -0,0 +1,38 @@ +// @ts-nocheck +import RecipeModule from "../../recipeModule"; +import { NormalisedAppinfo, APIHandled, RecipeListFunction, HTTPMethod, UserContext } from "../../types"; +import { TypeInput, TypeNormalisedInput, RecipeInterface, APIInterface } from "./types"; +import STError from "../../error"; +import NormalisedURLPath from "../../normalisedURLPath"; +import type { BaseRequest, BaseResponse } from "../../framework"; +export default class Recipe extends RecipeModule { + private static instance; + static RECIPE_ID: string; + config: TypeNormalisedInput; + recipeInterfaceImpl: RecipeInterface; + apiImpl: APIInterface; + isInServerlessEnv: boolean; + constructor( + recipeId: string, + appInfo: NormalisedAppinfo, + isInServerlessEnv: boolean, + config: TypeInput, + _recipes: {} + ); + static init(config: TypeInput): RecipeListFunction; + static getInstanceOrThrowError(): Recipe; + static reset(): void; + getAPIsHandled: () => APIHandled[]; + handleAPIRequest: ( + id: string, + tenantId: string, + req: BaseRequest, + res: BaseResponse, + _path: NormalisedURLPath, + _method: HTTPMethod, + userContext: UserContext + ) => Promise; + handleError: (err: STError, _request: BaseRequest, _response: BaseResponse) => Promise; + getAllCORSHeaders: () => string[]; + isErrorFromThisRecipe: (err: any) => err is STError; +} diff --git a/lib/build/recipe/oauth2client/recipe.js b/lib/build/recipe/oauth2client/recipe.js new file mode 100644 index 000000000..daf6b07f3 --- /dev/null +++ b/lib/build/recipe/oauth2client/recipe.js @@ -0,0 +1,107 @@ +"use strict"; +/* Copyright (c) 2024, 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 recipeModule_1 = __importDefault(require("../../recipeModule")); +const utils_1 = require("./utils"); +const error_1 = __importDefault(require("../../error")); +const constants_1 = require("./constants"); +const normalisedURLPath_1 = __importDefault(require("../../normalisedURLPath")); +const signin_1 = __importDefault(require("./api/signin")); +const recipeImplementation_1 = __importDefault(require("./recipeImplementation")); +const implementation_1 = __importDefault(require("./api/implementation")); +const querier_1 = require("../../querier"); +const supertokens_js_override_1 = __importDefault(require("supertokens-js-override")); +class Recipe extends recipeModule_1.default { + constructor(recipeId, appInfo, isInServerlessEnv, config, _recipes) { + super(recipeId, appInfo); + this.getAPIsHandled = () => { + return [ + { + method: "post", + pathWithoutApiBasePath: new normalisedURLPath_1.default(constants_1.SIGN_IN_API), + id: constants_1.SIGN_IN_API, + disabled: this.apiImpl.signInPOST === undefined, + }, + ]; + }; + this.handleAPIRequest = async (id, tenantId, req, res, _path, _method, userContext) => { + let options = { + config: this.config, + recipeId: this.getRecipeId(), + isInServerlessEnv: this.isInServerlessEnv, + recipeImplementation: this.recipeInterfaceImpl, + req, + res, + appInfo: this.getAppInfo(), + }; + if (id === constants_1.SIGN_IN_API) { + return await signin_1.default(this.apiImpl, tenantId, options, userContext); + } + return false; + }; + this.handleError = async (err, _request, _response) => { + throw err; + }; + this.getAllCORSHeaders = () => { + return []; + }; + this.isErrorFromThisRecipe = (err) => { + return error_1.default.isErrorFromSuperTokens(err) && err.fromRecipe === Recipe.RECIPE_ID; + }; + this.config = utils_1.validateAndNormaliseUserInput(appInfo, config); + this.isInServerlessEnv = isInServerlessEnv; + { + let builder = new supertokens_js_override_1.default( + recipeImplementation_1.default(querier_1.Querier.getNewInstanceOrThrowError(recipeId), this.config) + ); + 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(); + } + } + static init(config) { + return (appInfo, isInServerlessEnv) => { + if (Recipe.instance === undefined) { + Recipe.instance = new Recipe(Recipe.RECIPE_ID, appInfo, isInServerlessEnv, config, {}); + return Recipe.instance; + } else { + throw new Error("OAuth2Client recipe has already been initialised. Please check your code for bugs."); + } + }; + } + static getInstanceOrThrowError() { + if (Recipe.instance !== undefined) { + return Recipe.instance; + } + throw new Error("Initialisation not done. Did you forget to call the OAuth2Client.init function?"); + } + static reset() { + if (process.env.TEST_MODE !== "testing") { + throw new Error("calling testing function in non testing env"); + } + Recipe.instance = undefined; + } +} +exports.default = Recipe; +Recipe.instance = undefined; +Recipe.RECIPE_ID = "oauth2client"; diff --git a/lib/build/recipe/oauth2client/recipeImplementation.d.ts b/lib/build/recipe/oauth2client/recipeImplementation.d.ts new file mode 100644 index 000000000..24599a2c7 --- /dev/null +++ b/lib/build/recipe/oauth2client/recipeImplementation.d.ts @@ -0,0 +1,4 @@ +// @ts-nocheck +import { RecipeInterface, TypeNormalisedInput } from "./types"; +import { Querier } from "../../querier"; +export default function getRecipeImplementation(_querier: Querier, config: TypeNormalisedInput): RecipeInterface; diff --git a/lib/build/recipe/oauth2client/recipeImplementation.js b/lib/build/recipe/oauth2client/recipeImplementation.js new file mode 100644 index 000000000..e935c9e60 --- /dev/null +++ b/lib/build/recipe/oauth2client/recipeImplementation.js @@ -0,0 +1,139 @@ +"use strict"; +var __importDefault = + (this && this.__importDefault) || + function (mod) { + return mod && mod.__esModule ? mod : { default: mod }; + }; +Object.defineProperty(exports, "__esModule", { value: true }); +const recipeUserId_1 = __importDefault(require("../../recipeUserId")); +const thirdpartyUtils_1 = require("../../thirdpartyUtils"); +const __1 = require("../.."); +const logger_1 = require("../../logger"); +const jose_1 = require("jose"); +function getRecipeImplementation(_querier, config) { + let providerConfigWithOIDCInfo = null; + return { + signIn: async function ({ userId, tenantId, userContext, oAuthTokens, rawUserInfo }) { + const user = await __1.getUser(userId, userContext); + if (user === undefined) { + throw new Error(`Failed to getUser from the userId ${userId} in the ${tenantId} tenant`); + } + return { + status: "OK", + user, + recipeUserId: new recipeUserId_1.default(userId), + oAuthTokens, + rawUserInfo, + }; + }, + getProviderConfig: async function () { + if (providerConfigWithOIDCInfo !== null) { + return providerConfigWithOIDCInfo; + } + const oidcInfo = await thirdpartyUtils_1.getOIDCDiscoveryInfo(config.providerConfig.oidcDiscoveryEndpoint); + if (oidcInfo.authorization_endpoint === undefined) { + throw new Error("Failed to authorization_endpoint from the oidcDiscoveryEndpoint."); + } + if (oidcInfo.token_endpoint === undefined) { + throw new Error("Failed to token_endpoint from the oidcDiscoveryEndpoint."); + } + if (oidcInfo.userinfo_endpoint === undefined) { + throw new Error("Failed to userinfo_endpoint from the oidcDiscoveryEndpoint."); + } + if (oidcInfo.jwks_uri === undefined) { + throw new Error("Failed to jwks_uri from the oidcDiscoveryEndpoint."); + } + providerConfigWithOIDCInfo = Object.assign(Object.assign({}, config.providerConfig), { + authorizationEndpoint: oidcInfo.authorization_endpoint, + tokenEndpoint: oidcInfo.token_endpoint, + userInfoEndpoint: oidcInfo.userinfo_endpoint, + jwksURI: oidcInfo.jwks_uri, + }); + return providerConfigWithOIDCInfo; + }, + exchangeAuthCodeForOAuthTokens: async function ({ providerConfig, redirectURIInfo }) { + if (providerConfig.tokenEndpoint === undefined) { + throw new Error("OAuth2Client provider's tokenEndpoint is not configured."); + } + const tokenAPIURL = providerConfig.tokenEndpoint; + const accessTokenAPIParams = { + client_id: providerConfig.clientId, + redirect_uri: redirectURIInfo.redirectURI, + code: redirectURIInfo.redirectURIQueryParams["code"], + grant_type: "authorization_code", + }; + if (providerConfig.clientSecret !== undefined) { + accessTokenAPIParams["client_secret"] = providerConfig.clientSecret; + } + if (redirectURIInfo.pkceCodeVerifier !== undefined) { + accessTokenAPIParams["code_verifier"] = redirectURIInfo.pkceCodeVerifier; + } + const tokenResponse = await thirdpartyUtils_1.doPostRequest(tokenAPIURL, accessTokenAPIParams); + if (tokenResponse.status >= 400) { + logger_1.logDebugMessage( + `Received response with status ${tokenResponse.status} and body ${tokenResponse.stringResponse}` + ); + throw new Error( + `Received response with status ${tokenResponse.status} and body ${tokenResponse.stringResponse}` + ); + } + return tokenResponse.jsonResponse; + }, + getUserInfo: async function ({ providerConfig, oAuthTokens }) { + var _a, _b; + let jwks; + const accessToken = oAuthTokens["access_token"]; + const idToken = oAuthTokens["id_token"]; + let rawUserInfo = { + fromUserInfoAPI: {}, + fromIdTokenPayload: {}, + }; + if (idToken && providerConfig.jwksURI !== undefined) { + if (jwks === undefined) { + jwks = jose_1.createRemoteJWKSet(new URL(providerConfig.jwksURI)); + } + rawUserInfo.fromIdTokenPayload = await thirdpartyUtils_1.verifyIdTokenFromJWKSEndpointAndGetPayload( + idToken, + jwks, + { + audience: providerConfig.clientId, + } + ); + } + if (accessToken && providerConfig.userInfoEndpoint !== undefined) { + const headers = { + Authorization: "Bearer " + accessToken, + }; + const queryParams = {}; + const userInfoFromAccessToken = await thirdpartyUtils_1.doGetRequest( + providerConfig.userInfoEndpoint, + queryParams, + headers + ); + if (userInfoFromAccessToken.status >= 400) { + logger_1.logDebugMessage( + `Received response with status ${userInfoFromAccessToken.status} and body ${userInfoFromAccessToken.stringResponse}` + ); + throw new Error( + `Received response with status ${userInfoFromAccessToken.status} and body ${userInfoFromAccessToken.stringResponse}` + ); + } + rawUserInfo.fromUserInfoAPI = userInfoFromAccessToken.jsonResponse; + } + let userId = undefined; + if (((_a = rawUserInfo.fromIdTokenPayload) === null || _a === void 0 ? void 0 : _a.sub) !== undefined) { + userId = rawUserInfo.fromIdTokenPayload["sub"]; + } else if (((_b = rawUserInfo.fromUserInfoAPI) === null || _b === void 0 ? void 0 : _b.sub) !== undefined) { + userId = rawUserInfo.fromUserInfoAPI["sub"]; + } + if (userId === undefined) { + throw new Error(`Failed to get userId from both the idToken and userInfo endpoint.`); + } + return { + userId, + rawUserInfo, + }; + }, + }; +} +exports.default = getRecipeImplementation; diff --git a/lib/build/recipe/oauth2client/types.d.ts b/lib/build/recipe/oauth2client/types.d.ts new file mode 100644 index 000000000..36b685837 --- /dev/null +++ b/lib/build/recipe/oauth2client/types.d.ts @@ -0,0 +1,154 @@ +// @ts-nocheck +import type { BaseRequest, BaseResponse } from "../../framework"; +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"; +export declare type UserInfo = { + userId: string; + rawUserInfo: { + fromIdTokenPayload?: { + [key: string]: any; + }; + fromUserInfoAPI?: { + [key: string]: any; + }; + }; +}; +export declare type ProviderConfigInput = { + clientId: string; + clientSecret?: string; + oidcDiscoveryEndpoint: string; +}; +export declare type ProviderConfigWithOIDCInfo = ProviderConfigInput & { + authorizationEndpoint: string; + tokenEndpoint: string; + userInfoEndpoint: string; + jwksURI: string; +}; +export declare type OAuthTokens = { + access_token?: string; + id_token?: string; +}; +export declare type OAuthTokenResponse = { + access_token: string; + id_token?: string; + refresh_token?: string; + expires_in: number; + scope?: string; + token_type: string; +}; +export declare type TypeInput = { + providerConfig: ProviderConfigInput; + override?: { + functions?: ( + originalImplementation: RecipeInterface, + builder?: OverrideableBuilder + ) => RecipeInterface; + apis?: (originalImplementation: APIInterface, builder?: OverrideableBuilder) => APIInterface; + }; +}; +export declare type TypeNormalisedInput = { + providerConfig: ProviderConfigInput; + override: { + functions: ( + originalImplementation: RecipeInterface, + builder?: OverrideableBuilder + ) => RecipeInterface; + apis: (originalImplementation: APIInterface, builder?: OverrideableBuilder) => APIInterface; + }; +}; +export declare type RecipeInterface = { + getProviderConfig(input: { userContext: UserContext }): Promise; + signIn(input: { + userId: string; + oAuthTokens: OAuthTokens; + rawUserInfo: { + fromIdTokenPayload?: { + [key: string]: any; + }; + fromUserInfoAPI?: { + [key: string]: any; + }; + }; + tenantId: string; + userContext: UserContext; + }): Promise<{ + status: "OK"; + recipeUserId: RecipeUserId; + user: User; + oAuthTokens: OAuthTokens; + rawUserInfo: { + fromIdTokenPayload?: { + [key: string]: any; + }; + fromUserInfoAPI?: { + [key: string]: any; + }; + }; + }>; + exchangeAuthCodeForOAuthTokens(input: { + providerConfig: ProviderConfigWithOIDCInfo; + redirectURIInfo: { + redirectURI: string; + redirectURIQueryParams: any; + pkceCodeVerifier?: string | undefined; + }; + userContext: UserContext; + }): Promise; + getUserInfo(input: { + providerConfig: ProviderConfigWithOIDCInfo; + oAuthTokens: OAuthTokens; + userContext: UserContext; + }): Promise; +}; +export declare type APIOptions = { + recipeImplementation: RecipeInterface; + config: TypeNormalisedInput; + recipeId: string; + isInServerlessEnv: boolean; + req: BaseRequest; + res: BaseResponse; + appInfo: NormalisedAppinfo; +}; +export declare type APIInterface = { + signInPOST: ( + input: { + tenantId: string; + options: APIOptions; + userContext: UserContext; + } & ( + | { + redirectURIInfo: { + redirectURI: string; + redirectURIQueryParams: any; + pkceCodeVerifier?: string; + }; + } + | { + oAuthTokens: { + [key: string]: any; + }; + } + ) + ) => Promise< + | { + status: "OK"; + user: User; + session: SessionContainerInterface; + oAuthTokens: { + [key: string]: any; + }; + rawUserInfo: { + fromIdTokenPayload?: { + [key: string]: any; + }; + fromUserInfoAPI?: { + [key: string]: any; + }; + }; + } + | GeneralErrorResponse + >; +}; diff --git a/lib/build/recipe/oauth2client/types.js b/lib/build/recipe/oauth2client/types.js new file mode 100644 index 000000000..9f1237319 --- /dev/null +++ b/lib/build/recipe/oauth2client/types.js @@ -0,0 +1,16 @@ +"use strict"; +/* Copyright (c) 2024, 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. + */ +Object.defineProperty(exports, "__esModule", { value: true }); diff --git a/lib/build/recipe/oauth2client/utils.d.ts b/lib/build/recipe/oauth2client/utils.d.ts new file mode 100644 index 000000000..6a930e641 --- /dev/null +++ b/lib/build/recipe/oauth2client/utils.d.ts @@ -0,0 +1,7 @@ +// @ts-nocheck +import { NormalisedAppinfo } from "../../types"; +import { TypeInput, TypeNormalisedInput } from "./types"; +export declare function validateAndNormaliseUserInput( + _appInfo: NormalisedAppinfo, + config: TypeInput +): TypeNormalisedInput; diff --git a/lib/build/recipe/oauth2client/utils.js b/lib/build/recipe/oauth2client/utils.js new file mode 100644 index 000000000..0c5e2f39b --- /dev/null +++ b/lib/build/recipe/oauth2client/utils.js @@ -0,0 +1,46 @@ +"use strict"; +/* Copyright (c) 2024, 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. + */ +Object.defineProperty(exports, "__esModule", { value: true }); +exports.validateAndNormaliseUserInput = void 0; +function validateAndNormaliseUserInput(_appInfo, config) { + if (config === undefined || config.providerConfig === undefined) { + throw new Error("Please pass providerConfig argument in the OAuth2Client recipe."); + } + if (config.providerConfig.clientId === undefined) { + throw new Error("Please pass clientId argument in the OAuth2Client providerConfig."); + } + // TODO: Decide on the prefix and also if we will allow users to customise clientIds + // if (!config.providerConfig.clientId.startsWith("supertokens_")) { + // throw new Error( + // `Only Supertokens OAuth ClientIds are supported in the OAuth2Client recipe. For any other OAuth Clients use the thirdparty recipe.` + // ); + // } + if (config.providerConfig.oidcDiscoveryEndpoint === undefined) { + throw new Error("Please pass oidcDiscoveryEndpoint argument in the OAuth2Client providerConfig."); + } + let override = Object.assign( + { + functions: (originalImplementation) => originalImplementation, + apis: (originalImplementation) => originalImplementation, + }, + config === null || config === void 0 ? void 0 : config.override + ); + return { + providerConfig: config.providerConfig, + override, + }; +} +exports.validateAndNormaliseUserInput = validateAndNormaliseUserInput; diff --git a/lib/build/recipe/oauth2provider/OAuth2Client.d.ts b/lib/build/recipe/oauth2provider/OAuth2Client.d.ts new file mode 100644 index 000000000..f61f7c4f8 --- /dev/null +++ b/lib/build/recipe/oauth2provider/OAuth2Client.d.ts @@ -0,0 +1,170 @@ +// @ts-nocheck +import { OAuth2ClientOptions } from "./types"; +export declare class OAuth2Client { + /** + * OAuth 2.0 Client ID + * The ID is immutable. If no ID is provided, a UUID4 will be generated. + */ + clientId: string; + /** + * OAuth 2.0 Client Secret + * The secret will be included in the create request as cleartext, and then + * never again. The secret is kept in hashed format and is not recoverable once lost. + */ + clientSecret?: string; + /** + * OAuth 2.0 Client Name + * The human-readable name of the client to be presented to the end-user during authorization. + */ + clientName: string; + /** + * OAuth 2.0 Client Scope + * Scope is a string containing a space-separated list of scope values that the client + * can use when requesting access tokens. + */ + scope: string; + /** + * Array of redirect URIs + * StringSliceJSONFormat represents []string{} which is encoded to/from JSON for SQL storage. + */ + redirectUris: string[] | null; + /** + * Authorization Code Grant Access Token Lifespan + * NullDuration - ^[0-9]+(ns|us|ms|s|m|h)$ + */ + authorizationCodeGrantAccessTokenLifespan: string | null; + /** + * Authorization Code Grant ID Token Lifespan + * NullDuration - ^[0-9]+(ns|us|ms|s|m|h)$ + */ + authorizationCodeGrantIdTokenLifespan: string | null; + /** + * Authorization Code Grant Refresh Token Lifespan + * NullDuration - ^[0-9]+(ns|us|ms|s|m|h)$ + */ + authorizationCodeGrantRefreshTokenLifespan: string | null; + /** + * Client Credentials Grant Access Token Lifespan + * NullDuration - ^[0-9]+(ns|us|ms|s|m|h)$ + */ + clientCredentialsGrantAccessTokenLifespan: string | null; + /** + * Implicit Grant Access Token Lifespan + * NullDuration - ^[0-9]+(ns|us|ms|s|m|h)$ + */ + implicitGrantAccessTokenLifespan: string | null; + /** + * Implicit Grant ID Token Lifespan + * NullDuration - ^[0-9]+(ns|us|ms|s|m|h)$ + */ + implicitGrantIdTokenLifespan: string | null; + /** + * Refresh Token Grant Access Token Lifespan + * NullDuration - ^[0-9]+(ns|us|ms|s|m|h)$ + */ + refreshTokenGrantAccessTokenLifespan: string | null; + /** + * Refresh Token Grant ID Token Lifespan + * NullDuration - ^[0-9]+(ns|us|ms|s|m|h)$ + */ + refreshTokenGrantIdTokenLifespan: string | null; + /** + * Refresh Token Grant Refresh Token Lifespan + * NullDuration - ^[0-9]+(ns|us|ms|s|m|h)$ + */ + refreshTokenGrantRefreshTokenLifespan: string | null; + /** + * OAuth 2.0 Token Endpoint Authentication Method + * Requested Client Authentication method for the Token Endpoint. + */ + tokenEndpointAuthMethod: string; + /** + * OAuth 2.0 Client URI + * ClientURI is a URL string of a web page providing information about the client. + */ + clientUri: string; + /** + * Array of allowed CORS origins + * StringSliceJSONFormat represents []string{} which is encoded to/from JSON for SQL storage. + */ + allowedCorsOrigins: string[]; + /** + * Array of audiences + * StringSliceJSONFormat represents []string{} which is encoded to/from JSON for SQL storage. + */ + audience: string[]; + /** + * Array of grant types + * StringSliceJSONFormat represents []string{} which is encoded to/from JSON for SQL storage. + */ + grantTypes: string[] | null; + /** + * Array of response types + * StringSliceJSONFormat represents []string{} which is encoded to/from JSON for SQL storage. + */ + responseTypes: string[] | null; + /** + * OAuth 2.0 Client Logo URI + * A URL string referencing the client's logo. + */ + logoUri: string; + /** + * OAuth 2.0 Client Policy URI + * PolicyURI is a URL string that points to a human-readable privacy policy document + * that describes how the deployment organization collects, uses, + * retains, and discloses personal data. + */ + policyUri: string; + /** + * OAuth 2.0 Client Terms of Service URI + * A URL string pointing to a human-readable terms of service + * document for the client that describes a contractual relationship + * between the end-user and the client that the end-user accepts when + * authorizing the client. + */ + tosUri: string; + /** + * OAuth 2.0 Client Creation Date + * CreatedAt returns the timestamp of the client's creation. + */ + createdAt: string; + /** + * OAuth 2.0 Client Last Update Date + * UpdatedAt returns the timestamp of the last update. + */ + updatedAt: string; + /** + * Metadata - JSON object + * JSONRawMessage represents a json.RawMessage that works well with JSON, SQL, and Swagger. + */ + metadata: Record; + constructor({ + clientId, + clientSecret, + clientName, + scope, + redirectUris, + authorizationCodeGrantAccessTokenLifespan, + authorizationCodeGrantIdTokenLifespan, + authorizationCodeGrantRefreshTokenLifespan, + clientCredentialsGrantAccessTokenLifespan, + implicitGrantAccessTokenLifespan, + implicitGrantIdTokenLifespan, + refreshTokenGrantAccessTokenLifespan, + refreshTokenGrantIdTokenLifespan, + refreshTokenGrantRefreshTokenLifespan, + tokenEndpointAuthMethod, + clientUri, + allowedCorsOrigins, + audience, + grantTypes, + responseTypes, + logoUri, + policyUri, + tosUri, + createdAt, + updatedAt, + metadata, + }: OAuth2ClientOptions); + static fromAPIResponse(response: any): OAuth2Client; +} diff --git a/lib/build/recipe/oauth2provider/OAuth2Client.js b/lib/build/recipe/oauth2provider/OAuth2Client.js new file mode 100644 index 000000000..4e9c91e41 --- /dev/null +++ b/lib/build/recipe/oauth2provider/OAuth2Client.js @@ -0,0 +1,84 @@ +"use strict"; +/* Copyright (c) 2024, 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. + */ +Object.defineProperty(exports, "__esModule", { value: true }); +exports.OAuth2Client = void 0; +const utils_1 = require("../../utils"); +class OAuth2Client { + constructor({ + clientId, + clientSecret, + clientName, + scope, + redirectUris = null, + authorizationCodeGrantAccessTokenLifespan = null, + authorizationCodeGrantIdTokenLifespan = null, + authorizationCodeGrantRefreshTokenLifespan = null, + clientCredentialsGrantAccessTokenLifespan = null, + implicitGrantAccessTokenLifespan = null, + implicitGrantIdTokenLifespan = null, + refreshTokenGrantAccessTokenLifespan = null, + refreshTokenGrantIdTokenLifespan = null, + refreshTokenGrantRefreshTokenLifespan = null, + tokenEndpointAuthMethod, + clientUri = "", + allowedCorsOrigins = [], + audience = [], + grantTypes = null, + responseTypes = null, + logoUri = "", + policyUri = "", + tosUri = "", + createdAt, + updatedAt, + metadata = {}, + }) { + /** + * Metadata - JSON object + * JSONRawMessage represents a json.RawMessage that works well with JSON, SQL, and Swagger. + */ + this.metadata = {}; + this.clientId = clientId; + this.clientSecret = clientSecret; + this.clientName = clientName; + this.scope = scope; + this.redirectUris = redirectUris; + this.authorizationCodeGrantAccessTokenLifespan = authorizationCodeGrantAccessTokenLifespan; + this.authorizationCodeGrantIdTokenLifespan = authorizationCodeGrantIdTokenLifespan; + this.authorizationCodeGrantRefreshTokenLifespan = authorizationCodeGrantRefreshTokenLifespan; + this.clientCredentialsGrantAccessTokenLifespan = clientCredentialsGrantAccessTokenLifespan; + this.implicitGrantAccessTokenLifespan = implicitGrantAccessTokenLifespan; + this.implicitGrantIdTokenLifespan = implicitGrantIdTokenLifespan; + this.refreshTokenGrantAccessTokenLifespan = refreshTokenGrantAccessTokenLifespan; + this.refreshTokenGrantIdTokenLifespan = refreshTokenGrantIdTokenLifespan; + this.refreshTokenGrantRefreshTokenLifespan = refreshTokenGrantRefreshTokenLifespan; + this.tokenEndpointAuthMethod = tokenEndpointAuthMethod; + this.clientUri = clientUri; + this.allowedCorsOrigins = allowedCorsOrigins; + this.audience = audience; + this.grantTypes = grantTypes; + this.responseTypes = responseTypes; + this.logoUri = logoUri; + this.policyUri = policyUri; + this.tosUri = tosUri; + this.createdAt = createdAt; + this.updatedAt = updatedAt; + this.metadata = metadata; + } + static fromAPIResponse(response) { + return new OAuth2Client(utils_1.transformObjectKeys(response, "camelCase")); + } +} +exports.OAuth2Client = OAuth2Client; diff --git a/lib/build/recipe/oauth2provider/api/auth.d.ts b/lib/build/recipe/oauth2provider/api/auth.d.ts new file mode 100644 index 000000000..059876918 --- /dev/null +++ b/lib/build/recipe/oauth2provider/api/auth.d.ts @@ -0,0 +1,8 @@ +// @ts-nocheck +import { APIInterface, APIOptions } from ".."; +import { UserContext } from "../../../types"; +export default function authGET( + apiImplementation: APIInterface, + options: APIOptions, + userContext: UserContext +): Promise; diff --git a/lib/build/recipe/oauth2provider/api/auth.js b/lib/build/recipe/oauth2provider/api/auth.js new file mode 100644 index 000000000..a064f9bb4 --- /dev/null +++ b/lib/build/recipe/oauth2provider/api/auth.js @@ -0,0 +1,78 @@ +"use strict"; +/* Copyright (c) 2024, 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 utils_1 = require("../../../utils"); +const set_cookie_parser_1 = __importDefault(require("set-cookie-parser")); +const session_1 = __importDefault(require("../../session")); +const error_1 = __importDefault(require("../../../recipe/session/error")); +async function authGET(apiImplementation, options, userContext) { + if (apiImplementation.authGET === undefined) { + return false; + } + const origURL = options.req.getOriginalURL(); + const splitURL = origURL.split("?"); + const params = new URLSearchParams(splitURL[1]); + let session, shouldTryRefresh; + try { + session = await session_1.default.getSession(options.req, options.res, { sessionRequired: false }, userContext); + shouldTryRefresh = false; + } catch (error) { + session = undefined; + if (error_1.default.isErrorFromSuperTokens(error) && error.type === error_1.default.TRY_REFRESH_TOKEN) { + shouldTryRefresh = true; + } else { + // This should generally not happen, but we can handle this as if the session is not present, + // because then we redirect to the frontend, which should handle the validation error + shouldTryRefresh = false; + } + } + let response = await apiImplementation.authGET({ + options, + params: Object.fromEntries(params.entries()), + cookie: options.req.getHeaderValue("cookie"), + session, + shouldTryRefresh, + userContext, + }); + if ("redirectTo" in response) { + if (response.setCookie) { + const cookieStr = set_cookie_parser_1.default.splitCookiesString(response.setCookie); + const cookies = set_cookie_parser_1.default.parse(cookieStr); + for (const cookie of cookies) { + options.res.setCookie( + cookie.name, + cookie.value, + cookie.domain, + !!cookie.secure, + !!cookie.httpOnly, + new Date(cookie.expires).getTime(), + cookie.path || "/", + cookie.sameSite + ); + } + } + options.res.original.redirect(response.redirectTo); + } else { + utils_1.send200Response(options.res, response); + } + return true; +} +exports.default = authGET; diff --git a/lib/build/recipe/oauth2provider/api/implementation.d.ts b/lib/build/recipe/oauth2provider/api/implementation.d.ts new file mode 100644 index 000000000..0218549fa --- /dev/null +++ b/lib/build/recipe/oauth2provider/api/implementation.d.ts @@ -0,0 +1,3 @@ +// @ts-nocheck +import { APIInterface } from "../types"; +export default function getAPIImplementation(): APIInterface; diff --git a/lib/build/recipe/oauth2provider/api/implementation.js b/lib/build/recipe/oauth2provider/api/implementation.js new file mode 100644 index 000000000..1ba2baf4d --- /dev/null +++ b/lib/build/recipe/oauth2provider/api/implementation.js @@ -0,0 +1,118 @@ +"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. + */ +Object.defineProperty(exports, "__esModule", { value: true }); +const utils_1 = require("./utils"); +function getAPIImplementation() { + return { + loginGET: async ({ loginChallenge, options, session, shouldTryRefresh, userContext }) => { + const response = await utils_1.loginGET({ + recipeImplementation: options.recipeImplementation, + loginChallenge, + session, + shouldTryRefresh, + isDirectCall: true, + userContext, + }); + return utils_1.handleInternalRedirects({ + response, + cookie: options.req.getHeaderValue("cookie"), + recipeImplementation: options.recipeImplementation, + session, + shouldTryRefresh, + userContext, + }); + }, + authGET: async ({ options, params, cookie, session, shouldTryRefresh, userContext }) => { + const response = await options.recipeImplementation.authorization({ + params, + cookies: cookie, + session, + userContext, + }); + if (response.status === "OK") { + return utils_1.handleInternalRedirects({ + response, + recipeImplementation: options.recipeImplementation, + cookie, + session, + shouldTryRefresh, + userContext, + }); + } + return response; + }, + tokenPOST: async (input) => { + return input.options.recipeImplementation.tokenExchange({ + authorizationHeader: input.authorizationHeader, + body: input.body, + userContext: input.userContext, + }); + }, + loginInfoGET: async ({ loginChallenge, options, userContext }) => { + const { client } = await options.recipeImplementation.getLoginRequest({ + challenge: loginChallenge, + userContext, + }); + return { + status: "OK", + info: { + clientId: client.clientId, + clientName: client.clientName, + tosUri: client.tosUri, + policyUri: client.policyUri, + logoUri: client.logoUri, + clientUri: client.clientUri, + metadata: client.metadata, + }, + }; + }, + userInfoGET: async ({ accessTokenPayload, user, scopes, tenantId, options, userContext }) => { + return options.recipeImplementation.buildUserInfo({ + user, + accessTokenPayload, + scopes, + tenantId, + userContext, + }); + }, + revokeTokenPOST: async (input) => { + if ("authorizationHeader" in input && input.authorizationHeader !== undefined) { + return input.options.recipeImplementation.revokeToken({ + token: input.token, + authorizationHeader: input.authorizationHeader, + userContext: input.userContext, + }); + } else if ("clientId" in input && input.clientId !== undefined) { + return input.options.recipeImplementation.revokeToken({ + token: input.token, + clientId: input.clientId, + clientSecret: input.clientSecret, + userContext: input.userContext, + }); + } else { + throw new Error(`Either of 'authorizationHeader' or 'clientId' must be provided`); + } + }, + introspectTokenPOST: async (input) => { + return input.options.recipeImplementation.introspectToken({ + token: input.token, + scopes: input.scopes, + userContext: input.userContext, + }); + }, + }; +} +exports.default = getAPIImplementation; diff --git a/lib/build/recipe/oauth2provider/api/introspectToken.d.ts b/lib/build/recipe/oauth2provider/api/introspectToken.d.ts new file mode 100644 index 000000000..3d2972c0d --- /dev/null +++ b/lib/build/recipe/oauth2provider/api/introspectToken.d.ts @@ -0,0 +1,8 @@ +// @ts-nocheck +import { APIInterface, APIOptions } from ".."; +import { UserContext } from "../../../types"; +export default function introspectTokenPOST( + apiImplementation: APIInterface, + options: APIOptions, + userContext: UserContext +): Promise; diff --git a/lib/build/recipe/oauth2provider/api/introspectToken.js b/lib/build/recipe/oauth2provider/api/introspectToken.js new file mode 100644 index 000000000..6e36c9c68 --- /dev/null +++ b/lib/build/recipe/oauth2provider/api/introspectToken.js @@ -0,0 +1,37 @@ +"use strict"; +/* Copyright (c) 2024, 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. + */ +Object.defineProperty(exports, "__esModule", { value: true }); +const utils_1 = require("../../../utils"); +async function introspectTokenPOST(apiImplementation, options, userContext) { + if (apiImplementation.introspectTokenPOST === undefined) { + return false; + } + const body = await options.req.getBodyAsJSONOrFormData(); + if (body.token === undefined) { + utils_1.sendNon200ResponseWithMessage(options.res, "token is required in the request body", 400); + return true; + } + const scopes = body.scope ? body.scope.split(" ") : []; + let response = await apiImplementation.introspectTokenPOST({ + options, + token: body.token, + scopes, + userContext, + }); + utils_1.send200Response(options.res, response); + return true; +} +exports.default = introspectTokenPOST; diff --git a/lib/build/recipe/oauth2provider/api/login.d.ts b/lib/build/recipe/oauth2provider/api/login.d.ts new file mode 100644 index 000000000..6f4253cef --- /dev/null +++ b/lib/build/recipe/oauth2provider/api/login.d.ts @@ -0,0 +1,8 @@ +// @ts-nocheck +import { APIInterface, APIOptions } from ".."; +import { UserContext } from "../../../types"; +export default function login( + apiImplementation: APIInterface, + options: APIOptions, + userContext: UserContext +): Promise; diff --git a/lib/build/recipe/oauth2provider/api/login.js b/lib/build/recipe/oauth2provider/api/login.js new file mode 100644 index 000000000..6ca65a5e3 --- /dev/null +++ b/lib/build/recipe/oauth2provider/api/login.js @@ -0,0 +1,86 @@ +"use strict"; +/* Copyright (c) 2024, 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 set_cookie_parser_1 = __importDefault(require("set-cookie-parser")); +const utils_1 = require("../../../utils"); +const session_1 = __importDefault(require("../../session")); +const error_1 = __importDefault(require("../../../error")); +const error_2 = __importDefault(require("../../../recipe/session/error")); +async function login(apiImplementation, options, userContext) { + var _a; + if (apiImplementation.loginGET === undefined) { + return false; + } + let session, shouldTryRefresh; + try { + session = await session_1.default.getSession(options.req, options.res, { sessionRequired: false }, userContext); + shouldTryRefresh = false; + } catch (error) { + // We can handle this as if the session is not present, because then we redirect to the frontend, + // which should handle the validation error + session = undefined; + if (error_1.default.isErrorFromSuperTokens(error) && error.type === error_2.default.TRY_REFRESH_TOKEN) { + shouldTryRefresh = true; + } else { + shouldTryRefresh = false; + } + } + const loginChallenge = + (_a = options.req.getKeyValueFromQuery("login_challenge")) !== null && _a !== void 0 + ? _a + : options.req.getKeyValueFromQuery("loginChallenge"); + if (loginChallenge === undefined) { + throw new error_1.default({ + type: error_1.default.BAD_INPUT_ERROR, + message: "Missing input param: loginChallenge", + }); + } + let response = await apiImplementation.loginGET({ + options, + loginChallenge, + session, + shouldTryRefresh, + userContext, + }); + if ("status" in response) { + utils_1.send200Response(options.res, response); + } else { + if (response.setCookie) { + const cookieStr = set_cookie_parser_1.default.splitCookiesString(response.setCookie); + const cookies = set_cookie_parser_1.default.parse(cookieStr); + for (const cookie of cookies) { + options.res.setCookie( + cookie.name, + cookie.value, + cookie.domain, + !!cookie.secure, + !!cookie.httpOnly, + new Date(cookie.expires).getTime(), + cookie.path || "/", + cookie.sameSite + ); + } + } + options.res.original.redirect(response.redirectTo); + } + return true; +} +exports.default = login; diff --git a/lib/build/recipe/oauth2provider/api/loginInfo.d.ts b/lib/build/recipe/oauth2provider/api/loginInfo.d.ts new file mode 100644 index 000000000..536858263 --- /dev/null +++ b/lib/build/recipe/oauth2provider/api/loginInfo.d.ts @@ -0,0 +1,8 @@ +// @ts-nocheck +import { APIInterface, APIOptions } from ".."; +import { UserContext } from "../../../types"; +export default function loginInfoGET( + apiImplementation: APIInterface, + options: APIOptions, + userContext: UserContext +): Promise; diff --git a/lib/build/recipe/oauth2provider/api/loginInfo.js b/lib/build/recipe/oauth2provider/api/loginInfo.js new file mode 100644 index 000000000..15b9da808 --- /dev/null +++ b/lib/build/recipe/oauth2provider/api/loginInfo.js @@ -0,0 +1,47 @@ +"use strict"; +/* Copyright (c) 2024, 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 utils_1 = require("../../../utils"); +const error_1 = __importDefault(require("../../../error")); +async function loginInfoGET(apiImplementation, options, userContext) { + var _a; + if (apiImplementation.loginInfoGET === undefined) { + return false; + } + const loginChallenge = + (_a = options.req.getKeyValueFromQuery("login_challenge")) !== null && _a !== void 0 + ? _a + : options.req.getKeyValueFromQuery("loginChallenge"); + if (loginChallenge === undefined) { + throw new error_1.default({ + type: error_1.default.BAD_INPUT_ERROR, + message: "Missing input param: loginChallenge", + }); + } + let response = await apiImplementation.loginInfoGET({ + options, + loginChallenge, + userContext, + }); + utils_1.send200Response(options.res, response); + return true; +} +exports.default = loginInfoGET; diff --git a/lib/build/recipe/oauth2provider/api/revokeToken.d.ts b/lib/build/recipe/oauth2provider/api/revokeToken.d.ts new file mode 100644 index 000000000..902e734d5 --- /dev/null +++ b/lib/build/recipe/oauth2provider/api/revokeToken.d.ts @@ -0,0 +1,8 @@ +// @ts-nocheck +import { APIInterface, APIOptions } from ".."; +import { UserContext } from "../../../types"; +export default function revokeTokenPOST( + apiImplementation: APIInterface, + options: APIOptions, + userContext: UserContext +): Promise; diff --git a/lib/build/recipe/oauth2provider/api/revokeToken.js b/lib/build/recipe/oauth2provider/api/revokeToken.js new file mode 100644 index 000000000..46fadd8aa --- /dev/null +++ b/lib/build/recipe/oauth2provider/api/revokeToken.js @@ -0,0 +1,51 @@ +"use strict"; +/* Copyright (c) 2024, 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. + */ +Object.defineProperty(exports, "__esModule", { value: true }); +const utils_1 = require("../../../utils"); +async function revokeTokenPOST(apiImplementation, options, userContext) { + if (apiImplementation.revokeTokenPOST === undefined) { + return false; + } + const body = await options.req.getBodyAsJSONOrFormData(); + if (body.token === undefined) { + utils_1.sendNon200ResponseWithMessage(options.res, "token is required in the request body", 400); + return true; + } + const authorizationHeader = options.req.getHeaderValue("authorization"); + if (authorizationHeader !== undefined && (body.client_id !== undefined || body.client_secret !== undefined)) { + utils_1.sendNon200ResponseWithMessage( + options.res, + "Only one of authorization header or client_id and client_secret can be provided", + 400 + ); + return true; + } + let response = await apiImplementation.revokeTokenPOST({ + options, + authorizationHeader, + token: body.token, + clientId: body.client_id, + clientSecret: body.client_secret, + userContext, + }); + if ("statusCode" in response && response.statusCode !== 200) { + utils_1.sendNon200Response(options.res, response.statusCode, response); + } else { + utils_1.send200Response(options.res, response); + } + return true; +} +exports.default = revokeTokenPOST; diff --git a/lib/build/recipe/oauth2provider/api/token.d.ts b/lib/build/recipe/oauth2provider/api/token.d.ts new file mode 100644 index 000000000..c697b7744 --- /dev/null +++ b/lib/build/recipe/oauth2provider/api/token.d.ts @@ -0,0 +1,8 @@ +// @ts-nocheck +import { APIInterface, APIOptions } from ".."; +import { UserContext } from "../../../types"; +export default function tokenPOST( + apiImplementation: APIInterface, + options: APIOptions, + userContext: UserContext +): Promise; diff --git a/lib/build/recipe/oauth2provider/api/token.js b/lib/build/recipe/oauth2provider/api/token.js new file mode 100644 index 000000000..3ff27158c --- /dev/null +++ b/lib/build/recipe/oauth2provider/api/token.js @@ -0,0 +1,39 @@ +"use strict"; +/* Copyright (c) 2024, 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. + */ +Object.defineProperty(exports, "__esModule", { value: true }); +const utils_1 = require("../../../utils"); +async function tokenPOST(apiImplementation, options, userContext) { + if (apiImplementation.tokenPOST === undefined) { + return false; + } + const authorizationHeader = options.req.getHeaderValue("authorization"); + let response = await apiImplementation.tokenPOST({ + authorizationHeader, + options, + body: await options.req.getBodyAsJSONOrFormData(), + userContext, + }); + if ("statusCode" in response && response.statusCode !== 200) { + utils_1.sendNon200Response(options.res, response.statusCode, { + error: response.error, + error_description: response.errorDescription, + }); + } else { + utils_1.send200Response(options.res, response); + } + return true; +} +exports.default = tokenPOST; diff --git a/lib/build/recipe/oauth2provider/api/userInfo.d.ts b/lib/build/recipe/oauth2provider/api/userInfo.d.ts new file mode 100644 index 000000000..d0b8cdf4e --- /dev/null +++ b/lib/build/recipe/oauth2provider/api/userInfo.d.ts @@ -0,0 +1,9 @@ +// @ts-nocheck +import { APIInterface, APIOptions } from ".."; +import { UserContext } from "../../../types"; +export default function userInfoGET( + apiImplementation: APIInterface, + tenantId: string, + options: APIOptions, + userContext: UserContext +): Promise; diff --git a/lib/build/recipe/oauth2provider/api/userInfo.js b/lib/build/recipe/oauth2provider/api/userInfo.js new file mode 100644 index 000000000..cfa8f704b --- /dev/null +++ b/lib/build/recipe/oauth2provider/api/userInfo.js @@ -0,0 +1,84 @@ +"use strict"; +/* Copyright (c) 2024, 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 recipe_1 = __importDefault(require("../recipe")); +const utils_1 = require("../../../utils"); +const __1 = require("../../.."); +async function userInfoGET(apiImplementation, tenantId, options, userContext) { + if (apiImplementation.userInfoGET === undefined) { + return false; + } + const authHeader = options.req.getHeaderValue("authorization"); + if (authHeader === undefined || !authHeader.startsWith("Bearer ")) { + utils_1.sendNon200ResponseWithMessage(options.res, "Missing or invalid Authorization header", 401); + return true; + } + const accessToken = authHeader.replace(/^Bearer /, "").trim(); + let accessTokenPayload; + try { + const { + payload, + } = await recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.validateOAuth2AccessToken({ + token: accessToken, + userContext, + }); + accessTokenPayload = payload; + } catch (error) { + options.res.setHeader("WWW-Authenticate", 'Bearer error="invalid_token"', false); + options.res.setHeader("Access-Control-Expose-Headers", "WWW-Authenticate", true); + utils_1.sendNon200ResponseWithMessage(options.res, "Invalid or expired OAuth2 access token", 401); + return true; + } + if ( + accessTokenPayload === null || + typeof accessTokenPayload !== "object" || + typeof accessTokenPayload.sub !== "string" || + !Array.isArray(accessTokenPayload.scp) + ) { + options.res.setHeader("WWW-Authenticate", 'Bearer error="invalid_token"', false); + options.res.setHeader("Access-Control-Expose-Headers", "WWW-Authenticate", true); + utils_1.sendNon200ResponseWithMessage(options.res, "Malformed access token payload", 401); + return true; + } + const userId = accessTokenPayload.sub; + const user = await __1.getUser(userId, userContext); + if (user === undefined) { + options.res.setHeader("WWW-Authenticate", 'Bearer error="invalid_token"', false); + options.res.setHeader("Access-Control-Expose-Headers", "WWW-Authenticate", true); + utils_1.sendNon200ResponseWithMessage( + options.res, + "Couldn't find any user associated with the access token", + 401 + ); + return true; + } + const response = await apiImplementation.userInfoGET({ + accessTokenPayload, + user, + tenantId, + scopes: accessTokenPayload.scp, + options, + userContext, + }); + utils_1.send200Response(options.res, response); + return true; +} +exports.default = userInfoGET; diff --git a/lib/build/recipe/oauth2provider/api/utils.d.ts b/lib/build/recipe/oauth2provider/api/utils.d.ts new file mode 100644 index 000000000..cff72a21a --- /dev/null +++ b/lib/build/recipe/oauth2provider/api/utils.d.ts @@ -0,0 +1,45 @@ +// @ts-nocheck +import { UserContext } from "../../../types"; +import { SessionContainerInterface } from "../../session/types"; +import { RecipeInterface } from "../types"; +export declare function loginGET({ + recipeImplementation, + loginChallenge, + shouldTryRefresh, + session, + setCookie, + isDirectCall, + userContext, +}: { + recipeImplementation: RecipeInterface; + loginChallenge: string; + session?: SessionContainerInterface; + shouldTryRefresh: boolean; + setCookie?: string; + userContext: UserContext; + isDirectCall: boolean; +}): Promise<{ + redirectTo: string; + setCookie: string | undefined; +}>; +export declare function handleInternalRedirects({ + response, + recipeImplementation, + session, + shouldTryRefresh, + cookie, + userContext, +}: { + response: { + redirectTo: string; + setCookie: string | undefined; + }; + recipeImplementation: RecipeInterface; + session?: SessionContainerInterface; + shouldTryRefresh: boolean; + cookie?: string; + userContext: UserContext; +}): Promise<{ + redirectTo: string; + setCookie: string | undefined; +}>; diff --git a/lib/build/recipe/oauth2provider/api/utils.js b/lib/build/recipe/oauth2provider/api/utils.js new file mode 100644 index 000000000..762963794 --- /dev/null +++ b/lib/build/recipe/oauth2provider/api/utils.js @@ -0,0 +1,249 @@ +"use strict"; +var __importDefault = + (this && this.__importDefault) || + function (mod) { + return mod && mod.__esModule ? mod : { default: mod }; + }; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.handleInternalRedirects = exports.loginGET = void 0; +const supertokens_1 = __importDefault(require("../../../supertokens")); +const constants_1 = require("../../multitenancy/constants"); +const session_1 = require("../../session"); +const constants_2 = require("../constants"); +const set_cookie_parser_1 = __importDefault(require("set-cookie-parser")); +// API implementation for the loginGET function. +// Extracted for use in both apiImplementation and handleInternalRedirects. +async function loginGET({ + recipeImplementation, + loginChallenge, + shouldTryRefresh, + session, + setCookie, + isDirectCall, + userContext, +}) { + var _a, _b; + const loginRequest = await recipeImplementation.getLoginRequest({ + challenge: loginChallenge, + userContext, + }); + const sessionInfo = + session !== undefined + ? await session_1.getSessionInformation( + session === null || session === void 0 ? void 0 : session.getHandle() + ) + : undefined; + if (!sessionInfo) { + session = undefined; + } + const incomingAuthUrlQueryParams = new URLSearchParams(loginRequest.requestUrl.split("?")[1]); + const promptParam = + (_a = incomingAuthUrlQueryParams.get("prompt")) !== null && _a !== void 0 + ? _a + : incomingAuthUrlQueryParams.get("st_prompt"); + const maxAgeParam = incomingAuthUrlQueryParams.get("max_age"); + if (maxAgeParam !== null) { + try { + const maxAgeParsed = Number.parseInt(maxAgeParam); + if (maxAgeParsed < 0) { + const reject = await recipeImplementation.rejectLoginRequest({ + challenge: loginChallenge, + error: { + status: "OAUTH_ERROR", + error: "invalid_request", + errorDescription: "max_age cannot be negative", + }, + userContext, + }); + return { redirectTo: reject.redirectTo, setCookie }; + } + } catch (_c) { + const reject = await recipeImplementation.rejectLoginRequest({ + challenge: loginChallenge, + error: { + status: "OAUTH_ERROR", + error: "invalid_request", + errorDescription: "max_age must be an integer", + }, + userContext, + }); + return { redirectTo: reject.redirectTo, setCookie }; + } + } + const tenantIdParam = incomingAuthUrlQueryParams.get("tenant_id"); + if ( + session && + (["", undefined].includes(loginRequest.subject) || session.getUserId() === loginRequest.subject) && + (["", null].includes(tenantIdParam) || session.getTenantId() === tenantIdParam) && + (promptParam !== "login" || isDirectCall) && + (maxAgeParam === null || + (maxAgeParam === "0" && isDirectCall) || + Number.parseInt(maxAgeParam) * 1000 > Date.now() - sessionInfo.timeCreated) + ) { + const accept = await recipeImplementation.acceptLoginRequest({ + challenge: loginChallenge, + subject: session.getUserId(), + identityProviderSessionId: session.getHandle(), + remember: true, + rememberFor: 3600, + userContext, + }); + return { redirectTo: accept.redirectTo, setCookie }; + } + const appInfo = supertokens_1.default.getInstanceOrThrowError().appInfo; + const websiteDomain = appInfo + .getOrigin({ + request: undefined, + userContext: userContext, + }) + .getAsStringDangerous(); + const websiteBasePath = appInfo.websiteBasePath.getAsStringDangerous(); + if (shouldTryRefresh) { + const websiteDomain = appInfo + .getOrigin({ + request: undefined, + userContext: userContext, + }) + .getAsStringDangerous(); + const websiteBasePath = appInfo.websiteBasePath.getAsStringDangerous(); + const queryParamsForTryRefreshPage = new URLSearchParams({ + loginChallenge, + }); + return { + redirectTo: websiteDomain + websiteBasePath + `/try-refresh?${queryParamsForTryRefreshPage.toString()}`, + setCookie, + }; + } + if (promptParam === "none") { + const reject = await recipeImplementation.rejectLoginRequest({ + challenge: loginChallenge, + error: { + status: "OAUTH_ERROR", + error: "login_required", + errorDescription: + "The Authorization Server requires End-User authentication. Prompt 'none' was requested, but no existing or expired login session was found.", + }, + userContext, + }); + return { redirectTo: reject.redirectTo, setCookie }; + } + const queryParamsForAuthPage = new URLSearchParams({ + loginChallenge, + }); + if ((_b = loginRequest.oidcContext) === null || _b === void 0 ? void 0 : _b.login_hint) { + queryParamsForAuthPage.set("hint", loginRequest.oidcContext.login_hint); + } + if (session !== undefined || promptParam === "login") { + queryParamsForAuthPage.set("forceFreshAuth", "true"); + } + if (tenantIdParam !== null && tenantIdParam !== constants_1.DEFAULT_TENANT_ID) { + queryParamsForAuthPage.set("tenantId", tenantIdParam); + } + return { + redirectTo: websiteDomain + websiteBasePath + `?${queryParamsForAuthPage.toString()}`, + setCookie, + }; +} +exports.loginGET = loginGET; +function getMergedCookies({ cookie = "", setCookie }) { + if (!setCookie) { + return cookie; + } + const cookieMap = cookie.split(";").reduce((acc, curr) => { + const [name, value] = curr.split("="); + return Object.assign(Object.assign({}, acc), { [name.trim()]: value }); + }, {}); + const setCookies = set_cookie_parser_1.default.parse(set_cookie_parser_1.default.splitCookiesString(setCookie)); + for (const { name, value, expires } of setCookies) { + if (expires && new Date(expires) < new Date()) { + delete cookieMap[name]; + } else { + cookieMap[name] = value; + } + } + return Object.entries(cookieMap) + .map(([key, value]) => `${key}=${value}`) + .join(";"); +} +function mergeSetCookieHeaders(setCookie1, setCookie2) { + if (!setCookie1) { + return setCookie2 || ""; + } + if (!setCookie2 || setCookie1 === setCookie2) { + return setCookie1; + } + return `${setCookie1}, ${setCookie2}`; +} +function isInternalRedirect(redirectTo) { + const { apiDomain, apiBasePath } = supertokens_1.default.getInstanceOrThrowError().appInfo; + const basePath = `${apiDomain.getAsStringDangerous()}${apiBasePath.getAsStringDangerous()}`; + return [ + constants_2.LOGIN_PATH, + constants_2.AUTH_PATH, + constants_2.LOGIN_PATH.replace("oauth", "oauth2"), + constants_2.AUTH_PATH.replace("oauth", "oauth2"), + ].some((path) => redirectTo.startsWith(`${basePath}${path}`)); +} +// In the OAuth2 flow, we do several internal redirects. These redirects don't require a frontend-to-api-server round trip. +// If an internal redirect is identified, it's handled directly by this function. +// Currently, we only need to handle redirects to /oauth/login and /oauth/auth endpoints. +async function handleInternalRedirects({ + response, + recipeImplementation, + session, + shouldTryRefresh, + cookie = "", + userContext, +}) { + var _a; + if (!isInternalRedirect(response.redirectTo)) { + return response; + } + // Typically, there are no more than 2 internal redirects per API call but we are allowing upto 10. + // This safety net prevents infinite redirect loops in case there are more redirects than expected. + const maxRedirects = 10; + let redirectCount = 0; + while (redirectCount < maxRedirects && isInternalRedirect(response.redirectTo)) { + cookie = getMergedCookies({ cookie, setCookie: response.setCookie }); + const queryString = response.redirectTo.split("?")[1]; + const params = new URLSearchParams(queryString); + if (response.redirectTo.includes(constants_2.LOGIN_PATH)) { + const loginChallenge = + (_a = params.get("login_challenge")) !== null && _a !== void 0 ? _a : params.get("loginChallenge"); + if (!loginChallenge) { + throw new Error(`Expected loginChallenge in ${response.redirectTo}`); + } + const loginRes = await loginGET({ + recipeImplementation, + loginChallenge, + session, + shouldTryRefresh, + setCookie: response.setCookie, + isDirectCall: false, + userContext, + }); + response = { + redirectTo: loginRes.redirectTo, + setCookie: mergeSetCookieHeaders(loginRes.setCookie, response.setCookie), + }; + } else if (response.redirectTo.includes(constants_2.AUTH_PATH)) { + const authRes = await recipeImplementation.authorization({ + params: Object.fromEntries(params.entries()), + cookies: cookie, + session, + userContext, + }); + if (authRes.status === "OK") { + response = { + redirectTo: authRes.redirectTo, + setCookie: mergeSetCookieHeaders(authRes.setCookie, response.setCookie), + }; + } + } else { + throw new Error(`Unexpected internal redirect ${response.redirectTo}`); + } + redirectCount++; + } + return response; +} +exports.handleInternalRedirects = handleInternalRedirects; diff --git a/lib/build/recipe/oauth2provider/constants.d.ts b/lib/build/recipe/oauth2provider/constants.d.ts new file mode 100644 index 000000000..10dd83851 --- /dev/null +++ b/lib/build/recipe/oauth2provider/constants.d.ts @@ -0,0 +1,9 @@ +// @ts-nocheck +export declare const OAUTH2_BASE_PATH = "/oauth/"; +export declare const LOGIN_PATH = "/oauth/login"; +export declare const AUTH_PATH = "/oauth/auth"; +export declare const TOKEN_PATH = "/oauth/token"; +export declare const LOGIN_INFO_PATH = "/oauth/login/info"; +export declare const USER_INFO_PATH = "/oauth/userinfo"; +export declare const REVOKE_TOKEN_PATH = "/oauth/revoke"; +export declare const INTROSPECT_TOKEN_PATH = "/oauth/introspect"; diff --git a/lib/build/recipe/oauth2provider/constants.js b/lib/build/recipe/oauth2provider/constants.js new file mode 100644 index 000000000..df8d4441c --- /dev/null +++ b/lib/build/recipe/oauth2provider/constants.js @@ -0,0 +1,25 @@ +"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. + */ +Object.defineProperty(exports, "__esModule", { value: true }); +exports.INTROSPECT_TOKEN_PATH = exports.REVOKE_TOKEN_PATH = exports.USER_INFO_PATH = exports.LOGIN_INFO_PATH = exports.TOKEN_PATH = exports.AUTH_PATH = exports.LOGIN_PATH = exports.OAUTH2_BASE_PATH = void 0; +exports.OAUTH2_BASE_PATH = "/oauth/"; +exports.LOGIN_PATH = "/oauth/login"; +exports.AUTH_PATH = "/oauth/auth"; +exports.TOKEN_PATH = "/oauth/token"; +exports.LOGIN_INFO_PATH = "/oauth/login/info"; +exports.USER_INFO_PATH = "/oauth/userinfo"; +exports.REVOKE_TOKEN_PATH = "/oauth/revoke"; +exports.INTROSPECT_TOKEN_PATH = "/oauth/introspect"; diff --git a/lib/build/recipe/oauth2provider/index.d.ts b/lib/build/recipe/oauth2provider/index.d.ts new file mode 100644 index 000000000..7a6f3c8f3 --- /dev/null +++ b/lib/build/recipe/oauth2provider/index.d.ts @@ -0,0 +1,129 @@ +// @ts-nocheck +import Recipe from "./recipe"; +import { + APIInterface, + RecipeInterface, + APIOptions, + CreateOAuth2ClientInput, + UpdateOAuth2ClientInput, + DeleteOAuth2ClientInput, + GetOAuth2ClientsInput, +} from "./types"; +export default class Wrapper { + static init: typeof Recipe.init; + static getOAuth2Client( + clientId: string, + userContext?: Record + ): Promise< + | { + status: "OK"; + client: import("./OAuth2Client").OAuth2Client; + } + | { + status: "ERROR"; + error: string; + errorHint: string; + } + >; + static getOAuth2Clients( + input: GetOAuth2ClientsInput, + userContext?: Record + ): Promise< + | { + status: "OK"; + clients: import("./OAuth2Client").OAuth2Client[]; + nextPaginationToken?: string | undefined; + } + | { + status: "ERROR"; + error: string; + errorHint: string; + } + >; + static createOAuth2Client( + input: CreateOAuth2ClientInput, + userContext?: Record + ): Promise< + | { + status: "OK"; + client: import("./OAuth2Client").OAuth2Client; + } + | { + status: "ERROR"; + error: string; + errorHint: string; + } + >; + static updateOAuth2Client( + input: UpdateOAuth2ClientInput, + userContext?: Record + ): Promise< + | { + status: "OK"; + client: import("./OAuth2Client").OAuth2Client; + } + | { + status: "ERROR"; + error: string; + errorHint: string; + } + >; + static deleteOAuth2Client( + input: DeleteOAuth2ClientInput, + userContext?: Record + ): Promise< + | { + status: "OK"; + } + | { + status: "ERROR"; + error: string; + errorHint: string; + } + >; + static validateOAuth2AccessToken( + token: string, + requirements?: { + clientId?: string; + scopes?: string[]; + audience?: string; + }, + checkDatabase?: boolean, + userContext?: Record + ): Promise<{ + status: "OK"; + payload: import("../usermetadata").JSONObject; + }>; + static createTokenForClientCredentials( + clientId: string, + clientSecret: string, + scope?: string[], + audience?: string, + userContext?: Record + ): Promise; + static revokeToken( + token: string, + clientId: string, + clientSecret?: string, + userContext?: Record + ): Promise< + | import("./types").ErrorOAuth2 + | { + status: "OK"; + } + >; + static validateOAuth2RefreshToken( + token: string, + scopes?: string[], + userContext?: Record + ): Promise; +} +export declare let init: typeof Recipe.init; +export declare let getOAuth2Clients: typeof Wrapper.getOAuth2Clients; +export declare let createOAuth2Client: typeof Wrapper.createOAuth2Client; +export declare let updateOAuth2Client: typeof Wrapper.updateOAuth2Client; +export declare let deleteOAuth2Client: typeof Wrapper.deleteOAuth2Client; +export declare let validateOAuth2AccessToken: typeof Wrapper.validateOAuth2AccessToken; +export declare let createTokenForClientCredentials: typeof Wrapper.createTokenForClientCredentials; +export declare let revokeToken: typeof Wrapper.revokeToken; +export type { APIInterface, APIOptions, RecipeInterface }; diff --git a/lib/build/recipe/oauth2provider/index.js b/lib/build/recipe/oauth2provider/index.js new file mode 100644 index 000000000..3a769700e --- /dev/null +++ b/lib/build/recipe/oauth2provider/index.js @@ -0,0 +1,125 @@ +"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 }); +exports.revokeToken = exports.createTokenForClientCredentials = exports.validateOAuth2AccessToken = exports.deleteOAuth2Client = exports.updateOAuth2Client = exports.createOAuth2Client = exports.getOAuth2Clients = exports.init = void 0; +const utils_1 = require("../../utils"); +const recipe_1 = __importDefault(require("./recipe")); +class Wrapper { + static async getOAuth2Client(clientId, userContext) { + return await recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.getOAuth2Client({ + clientId, + userContext: utils_1.getUserContext(userContext), + }); + } + static async getOAuth2Clients(input, userContext) { + return await recipe_1.default + .getInstanceOrThrowError() + .recipeInterfaceImpl.getOAuth2Clients( + Object.assign(Object.assign({}, input), { userContext: utils_1.getUserContext(userContext) }) + ); + } + static async createOAuth2Client(input, userContext) { + return await recipe_1.default + .getInstanceOrThrowError() + .recipeInterfaceImpl.createOAuth2Client( + Object.assign(Object.assign({}, input), { userContext: utils_1.getUserContext(userContext) }) + ); + } + static async updateOAuth2Client(input, userContext) { + return await recipe_1.default + .getInstanceOrThrowError() + .recipeInterfaceImpl.updateOAuth2Client( + Object.assign(Object.assign({}, input), { userContext: utils_1.getUserContext(userContext) }) + ); + } + static async deleteOAuth2Client(input, userContext) { + return await recipe_1.default + .getInstanceOrThrowError() + .recipeInterfaceImpl.deleteOAuth2Client( + Object.assign(Object.assign({}, input), { userContext: utils_1.getUserContext(userContext) }) + ); + } + static validateOAuth2AccessToken(token, requirements, checkDatabase, userContext) { + return recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.validateOAuth2AccessToken({ + token, + requirements, + checkDatabase, + userContext: utils_1.getUserContext(userContext), + }); + } + static createTokenForClientCredentials(clientId, clientSecret, scope, audience, userContext) { + return recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.tokenExchange({ + body: { + grant_type: "client_credentials", + client_id: clientId, + client_secret: clientSecret, + scope: scope === null || scope === void 0 ? void 0 : scope.join(" "), + audience: audience, + }, + userContext: utils_1.getUserContext(userContext), + }); + } + static async revokeToken(token, clientId, clientSecret, userContext) { + let authorizationHeader = undefined; + const normalisedUserContext = utils_1.getUserContext(userContext); + const recipeInterfaceImpl = recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl; + const res = await recipeInterfaceImpl.getOAuth2Client({ clientId, userContext: normalisedUserContext }); + if (res.status !== "OK") { + throw new Error(`Failed to get OAuth2 client with id ${clientId}: ${res.error}`); + } + const { tokenEndpointAuthMethod } = res.client; + if (tokenEndpointAuthMethod === "none") { + authorizationHeader = "Basic " + Buffer.from(clientId + ":").toString("base64"); + } else if (tokenEndpointAuthMethod === "client_secret_basic") { + authorizationHeader = "Basic " + Buffer.from(clientId + ":" + clientSecret).toString("base64"); + } + if (authorizationHeader !== undefined) { + return await recipeInterfaceImpl.revokeToken({ + token, + authorizationHeader, + userContext: normalisedUserContext, + }); + } + return await recipeInterfaceImpl.revokeToken({ + token, + clientId, + clientSecret, + userContext: normalisedUserContext, + }); + } + static validateOAuth2RefreshToken(token, scopes, userContext) { + return recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.introspectToken({ + token, + scopes, + userContext: utils_1.getUserContext(userContext), + }); + } +} +exports.default = Wrapper; +Wrapper.init = recipe_1.default.init; +exports.init = Wrapper.init; +exports.getOAuth2Clients = Wrapper.getOAuth2Clients; +exports.createOAuth2Client = Wrapper.createOAuth2Client; +exports.updateOAuth2Client = Wrapper.updateOAuth2Client; +exports.deleteOAuth2Client = Wrapper.deleteOAuth2Client; +exports.validateOAuth2AccessToken = Wrapper.validateOAuth2AccessToken; +exports.createTokenForClientCredentials = Wrapper.createTokenForClientCredentials; +exports.revokeToken = Wrapper.revokeToken; diff --git a/lib/build/recipe/oauth2provider/recipe.d.ts b/lib/build/recipe/oauth2provider/recipe.d.ts new file mode 100644 index 000000000..25c12cc57 --- /dev/null +++ b/lib/build/recipe/oauth2provider/recipe.d.ts @@ -0,0 +1,68 @@ +// @ts-nocheck +import error from "../../error"; +import type { BaseRequest, BaseResponse } from "../../framework"; +import NormalisedURLPath from "../../normalisedURLPath"; +import RecipeModule from "../../recipeModule"; +import { APIHandled, HTTPMethod, JSONObject, NormalisedAppinfo, RecipeListFunction, UserContext } from "../../types"; +import { + APIInterface, + PayloadBuilderFunction, + RecipeInterface, + TypeInput, + TypeNormalisedInput, + UserInfo, + UserInfoBuilderFunction, +} from "./types"; +import { User } from "../../user"; +export default class Recipe extends RecipeModule { + static RECIPE_ID: string; + private static instance; + private accessTokenBuilders; + private idTokenBuilders; + private userInfoBuilders; + config: TypeNormalisedInput; + recipeInterfaceImpl: RecipeInterface; + apiImpl: APIInterface; + isInServerlessEnv: boolean; + constructor(recipeId: string, appInfo: NormalisedAppinfo, isInServerlessEnv: boolean, config?: TypeInput); + static getInstance(): Recipe | undefined; + static getInstanceOrThrowError(): Recipe; + static init(config?: TypeInput): RecipeListFunction; + static reset(): void; + addUserInfoBuilderFromOtherRecipe: (userInfoBuilderFn: UserInfoBuilderFunction) => void; + addAccessTokenBuilderFromOtherRecipe: (accessTokenBuilders: PayloadBuilderFunction) => void; + addIdTokenBuilderFromOtherRecipe: (idTokenBuilder: PayloadBuilderFunction) => void; + saveTokensForHook: (sessionHandle: string, idToken: JSONObject, accessToken: JSONObject) => void; + getAPIsHandled(): APIHandled[]; + handleAPIRequest: ( + id: string, + tenantId: string, + req: BaseRequest, + res: BaseResponse, + _path: NormalisedURLPath, + _method: HTTPMethod, + userContext: UserContext + ) => Promise; + handleError(error: error, _: BaseRequest, __: BaseResponse, _userContext: UserContext): Promise; + getAllCORSHeaders(): string[]; + isErrorFromThisRecipe(err: any): err is error; + getDefaultAccessTokenPayload( + user: User, + scopes: string[], + sessionHandle: string, + userContext: UserContext + ): Promise; + getDefaultIdTokenPayload( + user: User, + scopes: string[], + sessionHandle: string, + userContext: UserContext + ): Promise; + getDefaultUserInfoPayload( + user: User, + accessTokenPayload: JSONObject, + scopes: string[], + tenantId: string, + userContext: UserContext + ): Promise; +} diff --git a/lib/build/recipe/oauth2provider/recipe.js b/lib/build/recipe/oauth2provider/recipe.js new file mode 100644 index 000000000..1b1cc2f50 --- /dev/null +++ b/lib/build/recipe/oauth2provider/recipe.js @@ -0,0 +1,285 @@ +"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")); +const normalisedURLPath_1 = __importDefault(require("../../normalisedURLPath")); +const querier_1 = require("../../querier"); +const recipeModule_1 = __importDefault(require("../../recipeModule")); +const auth_1 = __importDefault(require("./api/auth")); +const implementation_1 = __importDefault(require("./api/implementation")); +const login_1 = __importDefault(require("./api/login")); +const token_1 = __importDefault(require("./api/token")); +const loginInfo_1 = __importDefault(require("./api/loginInfo")); +const constants_1 = require("./constants"); +const recipeImplementation_1 = __importDefault(require("./recipeImplementation")); +const utils_1 = require("./utils"); +const supertokens_js_override_1 = __importDefault(require("supertokens-js-override")); +const userInfo_1 = __importDefault(require("./api/userInfo")); +const combinedRemoteJWKSet_1 = require("../../combinedRemoteJWKSet"); +const revokeToken_1 = __importDefault(require("./api/revokeToken")); +const introspectToken_1 = __importDefault(require("./api/introspectToken")); +const session_1 = require("../session"); +const utils_2 = require("../../utils"); +const tokenHookMap = new Map(); +class Recipe extends recipeModule_1.default { + constructor(recipeId, appInfo, isInServerlessEnv, config) { + super(recipeId, appInfo); + this.accessTokenBuilders = []; + this.idTokenBuilders = []; + this.userInfoBuilders = []; + this.addUserInfoBuilderFromOtherRecipe = (userInfoBuilderFn) => { + this.userInfoBuilders.push(userInfoBuilderFn); + }; + this.addAccessTokenBuilderFromOtherRecipe = (accessTokenBuilders) => { + this.accessTokenBuilders.push(accessTokenBuilders); + }; + this.addIdTokenBuilderFromOtherRecipe = (idTokenBuilder) => { + this.idTokenBuilders.push(idTokenBuilder); + }; + this.saveTokensForHook = (sessionHandle, idToken, accessToken) => { + tokenHookMap.set(sessionHandle, { idToken, accessToken }); + }; + this.handleAPIRequest = async (id, tenantId, req, res, _path, _method, userContext) => { + let options = { + config: this.config, + recipeId: this.getRecipeId(), + isInServerlessEnv: this.isInServerlessEnv, + recipeImplementation: this.recipeInterfaceImpl, + req, + res, + }; + if (id === constants_1.LOGIN_PATH) { + return login_1.default(this.apiImpl, options, userContext); + } + if (id === constants_1.TOKEN_PATH) { + return token_1.default(this.apiImpl, options, userContext); + } + if (id === constants_1.AUTH_PATH) { + return auth_1.default(this.apiImpl, options, userContext); + } + if (id === constants_1.LOGIN_INFO_PATH) { + return loginInfo_1.default(this.apiImpl, options, userContext); + } + if (id === constants_1.USER_INFO_PATH) { + return userInfo_1.default(this.apiImpl, tenantId, options, userContext); + } + if (id === constants_1.REVOKE_TOKEN_PATH) { + return revokeToken_1.default(this.apiImpl, options, userContext); + } + if (id === constants_1.INTROSPECT_TOKEN_PATH) { + return introspectToken_1.default(this.apiImpl, options, userContext); + } + if (id === "token-hook") { + const body = await options.req.getBodyAsJSONOrFormData(); + const sessionHandle = body.session.extra.sessionHandle; + const tokens = tokenHookMap.get(sessionHandle); + if (tokens !== undefined) { + const { idToken, accessToken } = tokens; + utils_2.send200Response(options.res, { + session: { + access_token: accessToken, + id_token: idToken, + }, + }); + } else { + utils_2.send200Response(options.res, {}); + } + return true; + } + throw new Error("Should never come here: handleAPIRequest called with unknown id"); + }; + this.config = utils_1.validateAndNormaliseUserInput(this, appInfo, config); + this.isInServerlessEnv = isInServerlessEnv; + { + let builder = new supertokens_js_override_1.default( + recipeImplementation_1.default( + querier_1.Querier.getNewInstanceOrThrowError(recipeId), + this.config, + appInfo, + this.getDefaultAccessTokenPayload.bind(this), + this.getDefaultIdTokenPayload.bind(this), + this.getDefaultUserInfoPayload.bind(this), + this.saveTokensForHook.bind(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(); + } + } + /* Init functions */ + static getInstance() { + return Recipe.instance; + } + static getInstanceOrThrowError() { + if (Recipe.instance !== undefined) { + return Recipe.instance; + } + throw new Error("Initialisation not done. Did you forget to call the Jwt.init function?"); + } + static init(config) { + return (appInfo, isInServerlessEnv) => { + if (Recipe.instance === undefined) { + Recipe.instance = new Recipe(Recipe.RECIPE_ID, appInfo, isInServerlessEnv, config); + return Recipe.instance; + } else { + throw new Error("OAuth2Provider recipe has already been initialised. Please check your code for bugs."); + } + }; + } + static reset() { + if (process.env.TEST_MODE !== "testing") { + throw new Error("calling testing function in non testing env"); + } + combinedRemoteJWKSet_1.resetCombinedJWKS(); + Recipe.instance = undefined; + } + /* RecipeModule functions */ + getAPIsHandled() { + return [ + { + method: "get", + pathWithoutApiBasePath: new normalisedURLPath_1.default(constants_1.LOGIN_PATH), + id: constants_1.LOGIN_PATH, + disabled: this.apiImpl.loginGET === undefined, + }, + { + method: "post", + pathWithoutApiBasePath: new normalisedURLPath_1.default(constants_1.TOKEN_PATH), + id: constants_1.TOKEN_PATH, + disabled: this.apiImpl.tokenPOST === undefined, + }, + { + method: "get", + pathWithoutApiBasePath: new normalisedURLPath_1.default(constants_1.AUTH_PATH), + id: constants_1.AUTH_PATH, + disabled: this.apiImpl.authGET === undefined, + }, + { + method: "get", + pathWithoutApiBasePath: new normalisedURLPath_1.default(constants_1.LOGIN_INFO_PATH), + id: constants_1.LOGIN_INFO_PATH, + disabled: this.apiImpl.loginInfoGET === undefined, + }, + { + method: "get", + pathWithoutApiBasePath: new normalisedURLPath_1.default(constants_1.USER_INFO_PATH), + id: constants_1.USER_INFO_PATH, + disabled: this.apiImpl.userInfoGET === undefined, + }, + { + method: "post", + pathWithoutApiBasePath: new normalisedURLPath_1.default(constants_1.REVOKE_TOKEN_PATH), + id: constants_1.REVOKE_TOKEN_PATH, + disabled: this.apiImpl.revokeTokenPOST === undefined, + }, + { + method: "post", + pathWithoutApiBasePath: new normalisedURLPath_1.default(constants_1.INTROSPECT_TOKEN_PATH), + id: constants_1.INTROSPECT_TOKEN_PATH, + disabled: this.apiImpl.introspectTokenPOST === undefined, + }, + { + // TODO: remove this once we get core support + method: "post", + pathWithoutApiBasePath: new normalisedURLPath_1.default("/oauth/token-hook"), + id: "token-hook", + disabled: false, + }, + ]; + } + handleError(error, _, __, _userContext) { + throw error; + } + getAllCORSHeaders() { + return []; + } + isErrorFromThisRecipe(err) { + return error_1.default.isErrorFromSuperTokens(err) && err.fromRecipe === Recipe.RECIPE_ID; + } + async getDefaultAccessTokenPayload(user, scopes, sessionHandle, userContext) { + const sessionInfo = await session_1.getSessionInformation(sessionHandle); + if (sessionInfo === undefined) { + throw new Error("Session not found"); + } + let payload = { + tId: sessionInfo.tenantId, + rsub: sessionInfo.recipeUserId.getAsString(), + sessionHandle: sessionHandle, + }; + for (const fn of this.accessTokenBuilders) { + payload = Object.assign(Object.assign({}, payload), await fn(user, scopes, sessionHandle, userContext)); + } + return payload; + } + async getDefaultIdTokenPayload(user, scopes, sessionHandle, userContext) { + let payload = {}; + if (scopes.includes("email")) { + payload.email = user === null || user === void 0 ? void 0 : user.emails[0]; + payload.email_verified = user.loginMethods.some( + (lm) => lm.hasSameEmailAs(user === null || user === void 0 ? void 0 : user.emails[0]) && lm.verified + ); + } + if (scopes.includes("phoneNumber")) { + payload.phoneNumber = user === null || user === void 0 ? void 0 : user.phoneNumbers[0]; + payload.phoneNumber_verified = user.loginMethods.some( + (lm) => + lm.hasSamePhoneNumberAs(user === null || user === void 0 ? void 0 : user.phoneNumbers[0]) && + lm.verified + ); + } + for (const fn of this.idTokenBuilders) { + payload = Object.assign(Object.assign({}, payload), await fn(user, scopes, sessionHandle, userContext)); + } + return payload; + } + async getDefaultUserInfoPayload(user, accessTokenPayload, scopes, tenantId, userContext) { + let payload = { + sub: accessTokenPayload.sub, + }; + if (scopes.includes("email")) { + payload.email = user === null || user === void 0 ? void 0 : user.emails[0]; + payload.email_verified = user.loginMethods.some( + (lm) => lm.hasSameEmailAs(user === null || user === void 0 ? void 0 : user.emails[0]) && lm.verified + ); + } + if (scopes.includes("phoneNumber")) { + payload.phoneNumber = user === null || user === void 0 ? void 0 : user.phoneNumbers[0]; + payload.phoneNumber_verified = user.loginMethods.some( + (lm) => + lm.hasSamePhoneNumberAs(user === null || user === void 0 ? void 0 : user.phoneNumbers[0]) && + lm.verified + ); + } + for (const fn of this.userInfoBuilders) { + payload = Object.assign( + Object.assign({}, payload), + await fn(user, accessTokenPayload, scopes, tenantId, userContext) + ); + } + return payload; + } +} +exports.default = Recipe; +Recipe.RECIPE_ID = "oauth2provider"; +Recipe.instance = undefined; diff --git a/lib/build/recipe/oauth2provider/recipeImplementation.d.ts b/lib/build/recipe/oauth2provider/recipeImplementation.d.ts new file mode 100644 index 000000000..3b6fee065 --- /dev/null +++ b/lib/build/recipe/oauth2provider/recipeImplementation.d.ts @@ -0,0 +1,13 @@ +// @ts-nocheck +import { Querier } from "../../querier"; +import { JSONObject, NormalisedAppinfo } from "../../types"; +import { RecipeInterface, TypeNormalisedInput, PayloadBuilderFunction, UserInfoBuilderFunction } from "./types"; +export default function getRecipeInterface( + querier: Querier, + _config: TypeNormalisedInput, + appInfo: NormalisedAppinfo, + getDefaultAccessTokenPayload: PayloadBuilderFunction, + getDefaultIdTokenPayload: PayloadBuilderFunction, + getDefaultUserInfoPayload: UserInfoBuilderFunction, + saveTokensForHook: (sessionHandle: string, idToken: JSONObject, accessToken: JSONObject) => void +): RecipeInterface; diff --git a/lib/build/recipe/oauth2provider/recipeImplementation.js b/lib/build/recipe/oauth2provider/recipeImplementation.js new file mode 100644 index 000000000..8428130ce --- /dev/null +++ b/lib/build/recipe/oauth2provider/recipeImplementation.js @@ -0,0 +1,611 @@ +"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 __createBinding = + (this && this.__createBinding) || + (Object.create + ? function (o, m, k, k2) { + if (k2 === undefined) k2 = k; + Object.defineProperty(o, k2, { + enumerable: true, + get: function () { + return m[k]; + }, + }); + } + : function (o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; + }); +var __setModuleDefault = + (this && this.__setModuleDefault) || + (Object.create + ? function (o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); + } + : function (o, v) { + o["default"] = v; + }); +var __importStar = + (this && this.__importStar) || + function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) + for (var k in mod) + if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); + __setModuleDefault(result, mod); + return result; + }; +var __importDefault = + (this && this.__importDefault) || + function (mod) { + return mod && mod.__esModule ? mod : { default: mod }; + }; +Object.defineProperty(exports, "__esModule", { value: true }); +const jose = __importStar(require("jose")); +const normalisedURLPath_1 = __importDefault(require("../../normalisedURLPath")); +const querier_1 = require("../../querier"); +const utils_1 = require("../../utils"); +const OAuth2Client_1 = require("./OAuth2Client"); +const __1 = require("../.."); +const combinedRemoteJWKSet_1 = require("../../combinedRemoteJWKSet"); +const session_1 = require("../session"); +function getUpdatedRedirectTo(appInfo, redirectTo) { + if (redirectTo.includes("{apiDomain}")) { + return redirectTo.replace( + "{apiDomain}", + appInfo.apiDomain.getAsStringDangerous() + appInfo.apiBasePath.getAsStringDangerous() + ); + } + // TODO: Remove this core changes are done + return redirectTo + .replace( + querier_1.hydraPubDomain, + appInfo.apiDomain.getAsStringDangerous() + appInfo.apiBasePath.getAsStringDangerous() + ) + .replace("oauth2/", "oauth/"); +} +function getRecipeInterface( + querier, + _config, + appInfo, + getDefaultAccessTokenPayload, + getDefaultIdTokenPayload, + getDefaultUserInfoPayload, + saveTokensForHook +) { + return { + getLoginRequest: async function (input) { + const resp = await querier.sendGetRequest( + new normalisedURLPath_1.default("/recipe/oauth/auth/requests/login"), + { challenge: input.challenge }, + input.userContext + ); + return { + challenge: resp.challenge, + client: OAuth2Client_1.OAuth2Client.fromAPIResponse(resp.client), + oidcContext: resp.oidcContext, + requestUrl: resp.requestUrl, + requestedAccessTokenAudience: resp.requestedAccessTokenAudience, + requestedScope: resp.requestedScope, + sessionId: resp.sessionId, + skip: resp.skip, + subject: resp.subject, + }; + }, + acceptLoginRequest: async function (input) { + const resp = await querier.sendPutRequest( + new normalisedURLPath_1.default(`/recipe/oauth/auth/requests/login/accept`), + { + acr: input.acr, + amr: input.amr, + context: input.context, + extendSessionLifespan: input.extendSessionLifespan, + forceSubjectIdentifier: input.forceSubjectIdentifier, + identityProviderSessionId: input.identityProviderSessionId, + remember: input.remember, + rememberFor: input.rememberFor, + subject: input.subject, + }, + { + challenge: input.challenge, + }, + input.userContext + ); + return { + redirectTo: getUpdatedRedirectTo(appInfo, resp.redirectTo), + }; + }, + rejectLoginRequest: async function (input) { + const resp = await querier.sendPutRequest( + new normalisedURLPath_1.default(`/recipe/oauth/auth/requests/login/reject`), + { + error: input.error.error, + error_description: input.error.errorDescription, + status_code: input.error.statusCode, + }, + { + login_challenge: input.challenge, + }, + input.userContext + ); + return { + redirectTo: getUpdatedRedirectTo(appInfo, resp.redirectTo), + }; + }, + getConsentRequest: async function (input) { + const resp = await querier.sendGetRequest( + new normalisedURLPath_1.default("/recipe/oauth/auth/requests/consent"), + { challenge: input.challenge }, + input.userContext + ); + return { + acr: resp.acr, + amr: resp.amr, + challenge: resp.challenge, + client: OAuth2Client_1.OAuth2Client.fromAPIResponse(resp.client), + context: resp.context, + loginChallenge: resp.loginChallenge, + loginSessionId: resp.loginSessionId, + oidcContext: resp.oidcContext, + requestedAccessTokenAudience: resp.requestedAccessTokenAudience, + requestedScope: resp.requestedScope, + skip: resp.skip, + subject: resp.subject, + }; + }, + acceptConsentRequest: async function (input) { + const resp = await querier.sendPutRequest( + new normalisedURLPath_1.default(`/recipe/oauth/auth/requests/consent/accept`), + { + context: input.context, + grantAccessTokenAudience: input.grantAccessTokenAudience, + grantScope: input.grantScope, + handledAt: input.handledAt, + remember: input.remember, + rememberFor: input.rememberFor, + session: input.session, + }, + { + challenge: input.challenge, + }, + input.userContext + ); + return { + redirectTo: getUpdatedRedirectTo(appInfo, resp.redirectTo), + }; + }, + rejectConsentRequest: async function (input) { + const resp = await querier.sendPutRequest( + new normalisedURLPath_1.default(`/recipe/oauth/auth/requests/consent/reject`), + { + error: input.error.error, + error_description: input.error.errorDescription, + status_code: input.error.statusCode, + }, + { + consentChallenge: input.challenge, + }, + input.userContext + ); + return { + redirectTo: getUpdatedRedirectTo(appInfo, resp.redirectTo), + }; + }, + authorization: async function (input) { + var _a, _b; + if (input.session !== undefined) { + if (input.params.prompt === "none") { + input.params["st_prompt"] = "none"; + delete input.params.prompt; + } + } + const resp = await querier.sendPostRequest( + new normalisedURLPath_1.default(`/recipe/oauth/auth`), + { + params: input.params, + cookies: `${input.cookies}`, + }, + // { + // // TODO: if session is not set also clear the oauth2 cookie + // Cookie: `${input.cookies}`, + // }, + input.userContext + ); + if (resp.status === "OK") { + const redirectTo = getUpdatedRedirectTo(appInfo, resp.redirectTo); + if (redirectTo === undefined) { + throw new Error(resp.body); + } + const redirectToURL = new URL(redirectTo); + const consentChallenge = redirectToURL.searchParams.get("consent_challenge"); + if (consentChallenge !== null && input.session !== undefined) { + const consentRequest = await this.getConsentRequest({ + challenge: consentChallenge, + userContext: input.userContext, + }); + const user = await __1.getUser(input.session.getUserId()); + if (!user) { + throw new Error("Should not happen"); + } + const idToken = await this.buildIdTokenPayload({ + user, + client: consentRequest.client, + sessionHandle: input.session.getHandle(), + scopes: consentRequest.requestedScope || [], + userContext: input.userContext, + }); + const accessTokenPayload = await this.buildAccessTokenPayload({ + user, + client: consentRequest.client, + sessionHandle: input.session.getHandle(), + scopes: consentRequest.requestedScope || [], + userContext: input.userContext, + }); + const sessionInfo = await session_1.getSessionInformation(input.session.getHandle()); + if (!sessionInfo) { + throw new Error("Session not found"); + } + const consentRes = await this.acceptConsentRequest( + Object.assign(Object.assign({}, input), { + challenge: consentRequest.challenge, + grantAccessTokenAudience: consentRequest.requestedAccessTokenAudience, + grantScope: consentRequest.requestedScope, + remember: true, + session: { + id_token: idToken, + access_token: accessTokenPayload, + }, + handledAt: new Date(sessionInfo.timeCreated).toISOString(), + }) + ); + return { + redirectTo: consentRes.redirectTo, + setCookie: (_a = resp.cookies) !== null && _a !== void 0 ? _a : undefined, + }; + } + return { redirectTo, setCookie: (_b = resp.cookies) !== null && _b !== void 0 ? _b : undefined }; + } + return resp; + }, + tokenExchange: async function (input) { + var _a, _b; + const inputBody = {}; + for (const key in input.body) { + inputBody[key] = input.body[key]; + } + if (input.body.grant_type === "refresh_token") { + const scopes = + (_b = (_a = input.body.scope) === null || _a === void 0 ? void 0 : _a.split(" ")) !== null && + _b !== void 0 + ? _b + : []; + const tokenInfo = await this.introspectToken({ + token: input.body.refresh_token, + scopes, + userContext: input.userContext, + }); + if (tokenInfo.active === true) { + const sessionHandle = tokenInfo.ext.sessionHandle; + const clientInfo = await this.getOAuth2Client({ + clientId: tokenInfo.client_id, + userContext: input.userContext, + }); + if (clientInfo.status === "ERROR") { + return { + statusCode: 400, + error: clientInfo.error, + errorDescription: clientInfo.errorHint, + }; + } + const client = clientInfo.client; + const user = await __1.getUser(tokenInfo.sub); + if (!user) { + throw new Error("User not found"); + } + const idToken = await this.buildIdTokenPayload({ + user, + client, + sessionHandle: sessionHandle, + scopes, + userContext: input.userContext, + }); + const accessTokenPayload = await this.buildAccessTokenPayload({ + user, + client, + sessionHandle: sessionHandle, + scopes, + userContext: input.userContext, + }); + inputBody["session"] = { + id_token: idToken, + access_token: accessTokenPayload, + }; + saveTokensForHook(sessionHandle, idToken, accessTokenPayload); + } + } + if (input.authorizationHeader) { + inputBody["authorizationHeader"] = input.authorizationHeader; + } + const res = await querier.sendPostRequest( + new normalisedURLPath_1.default(`/recipe/oauth/token`), + { inputBody, iss: await this.getIssuer({ userContext: input.userContext }) }, + input.userContext + ); + if (res.status !== "OK") { + return { + statusCode: res.statusCode, + error: res.error, + errorDescription: res.errorDescription, + }; + } + return res; + }, + getOAuth2Clients: async function (input) { + var _a; + let response = await querier.sendGetRequestWithResponseHeaders( + new normalisedURLPath_1.default(`/recipe/oauth2/admin/clients`), + Object.assign(Object.assign({}, utils_1.transformObjectKeys(input, "snake-case")), { + page_token: input.paginationToken, + }), + {}, + input.userContext + ); + if (response.body.status === "OK") { + // Pagination info is in the Link header, containing comma-separated links: + // "first", "next" (if applicable). + // Example: Link: ; rel="first", ; rel="next" + // We parse the nextPaginationToken from the Link header using RegExp + let nextPaginationToken; + const linkHeader = (_a = response.headers.get("link")) !== null && _a !== void 0 ? _a : ""; + const nextLinkMatch = linkHeader.match(/<([^>]+)>;\s*rel="next"/); + if (nextLinkMatch) { + const url = nextLinkMatch[1]; + const urlParams = new URLSearchParams(url.split("?")[1]); + nextPaginationToken = urlParams.get("page_token"); + } + return { + status: "OK", + clients: response.body.data.map((client) => OAuth2Client_1.OAuth2Client.fromAPIResponse(client)), + nextPaginationToken, + }; + } else { + return { + status: "ERROR", + error: response.body.data.error, + errorHint: response.body.data.errorHint, + }; + } + }, + getOAuth2Client: async function (input) { + let response = await querier.sendGetRequestWithResponseHeaders( + new normalisedURLPath_1.default(`/recipe/oauth2/admin/clients/${input.clientId}`), + {}, + {}, + input.userContext + ); + if (response.body.status === "OK") { + return { + status: "OK", + client: OAuth2Client_1.OAuth2Client.fromAPIResponse(response.body.data), + }; + } else { + return { + status: "ERROR", + error: response.body.data.error, + errorHint: response.body.data.errorHint, + }; + } + }, + createOAuth2Client: async function (input) { + let response = await querier.sendPostRequest( + new normalisedURLPath_1.default(`/recipe/oauth2/admin/clients`), + Object.assign(Object.assign({}, utils_1.transformObjectKeys(input, "snake-case")), { + // TODO: these defaults should be set/enforced on the core side + access_token_strategy: "jwt", + skip_consent: true, + subject_type: "public", + }), + input.userContext + ); + if (response.status === "OK") { + return { + status: "OK", + client: OAuth2Client_1.OAuth2Client.fromAPIResponse(response.data), + }; + } else { + return { + status: "ERROR", + error: response.data.error, + errorHint: response.data.errorHint, + }; + } + }, + updateOAuth2Client: async function (input) { + // We convert the input into an array of "replace" operations + const requestBody = Object.entries(input).reduce((result, [key, value]) => { + result.push({ + from: `/${utils_1.toSnakeCase(key)}`, + op: "replace", + path: `/${utils_1.toSnakeCase(key)}`, + value, + }); + return result; + }, []); + let response = await querier.sendPatchRequest( + new normalisedURLPath_1.default(`/recipe/oauth2/admin/clients/${input.clientId}`), + requestBody, + input.userContext + ); + if (response.status === "OK") { + return { + status: "OK", + client: OAuth2Client_1.OAuth2Client.fromAPIResponse(response.data), + }; + } else { + return { + status: "ERROR", + error: response.data.error, + errorHint: response.data.errorHint, + }; + } + }, + deleteOAuth2Client: async function (input) { + let response = await querier.sendDeleteRequest( + new normalisedURLPath_1.default(`/recipe/oauth2/admin/clients/${input.clientId}`), + undefined, + undefined, + input.userContext + ); + if (response.status === "OK") { + return { status: "OK" }; + } else { + return { + status: "ERROR", + error: response.data.error, + errorHint: response.data.errorHint, + }; + } + }, + getIssuer: async function (_) { + return appInfo.apiDomain.getAsStringDangerous() + appInfo.apiBasePath.getAsStringDangerous(); + }, + buildAccessTokenPayload: async function (input) { + return getDefaultAccessTokenPayload(input.user, input.scopes, input.sessionHandle, input.userContext); + }, + buildIdTokenPayload: async function (input) { + return getDefaultIdTokenPayload(input.user, input.scopes, input.sessionHandle, input.userContext); + }, + buildUserInfo: async function ({ user, accessTokenPayload, scopes, tenantId, userContext }) { + return getDefaultUserInfoPayload(user, accessTokenPayload, scopes, tenantId, userContext); + }, + validateOAuth2AccessToken: async function (input) { + var _a, _b, _c, _d, _e; + const payload = (await jose.jwtVerify(input.token, combinedRemoteJWKSet_1.getCombinedJWKS())).payload; + // if (payload.stt !== 1) { + // throw new Error("Wrong token type"); + // } + // TODO: we should be able uncomment this after we get proper core support + // TODO: make this configurable? + // const expectedIssuer = + // appInfo.apiDomain.getAsStringDangerous() + appInfo.apiBasePath.getAsStringDangerous(); + // if (payload.iss !== expectedIssuer) { + // throw new Error("Issuer mismatch: this token was likely issued by another application or spoofed"); + // } + if ( + ((_a = input.requirements) === null || _a === void 0 ? void 0 : _a.clientId) !== undefined && + payload.client_id !== input.requirements.clientId + ) { + throw new Error("The token doesn't belong to the specified client"); + } + if ( + ((_b = input.requirements) === null || _b === void 0 ? void 0 : _b.scopes) !== undefined && + input.requirements.scopes.some((scope) => !payload.scp.includes(scope)) + ) { + throw new Error("The token is missing some required scopes"); + } + const aud = + payload.aud instanceof Array + ? payload.aud + : (_d = (_c = payload.aud) === null || _c === void 0 ? void 0 : _c.split(" ")) !== null && + _d !== void 0 + ? _d + : []; + if ( + ((_e = input.requirements) === null || _e === void 0 ? void 0 : _e.audience) !== undefined && + !aud.includes(input.requirements.audience) + ) { + throw new Error("The token doesn't belong to the specified audience"); + } + if (input.checkDatabase) { + let response = await querier.sendPostRequest( + new normalisedURLPath_1.default(`/recipe/oauth2/admin/oauth2/introspect`), + { + $isFormData: true, + token: input.token, + }, + input.userContext + ); + // TODO: fix after the core interface is there + if (response.status !== "OK" || response.data.active !== true) { + throw new Error(response.data.error); + } + } + return { status: "OK", payload: payload }; + }, + revokeToken: async function (input) { + const requestBody = { + $isFormData: true, + token: input.token, + }; + if ("authorizationHeader" in input && input.authorizationHeader !== undefined) { + requestBody.authorizationHeader = input.authorizationHeader; + } else { + if ("clientId" in input && input.clientId !== undefined) { + requestBody.client_id = input.clientId; + } + if ("clientSecret" in input && input.clientSecret !== undefined) { + requestBody.client_secret = input.clientSecret; + } + } + const res = await querier.sendPostRequest( + new normalisedURLPath_1.default(`/recipe/oauth2/pub/revoke`), + requestBody, + input.userContext + ); + if (res.status !== "OK") { + return { + status: "OAUTH_ERROR", + statusCode: res.statusCode, + error: res.data.error, + errorDescription: res.data.error_description, + }; + } + return { status: "OK" }; + }, + introspectToken: async function ({ token, scopes, userContext }) { + // Determine if the token is an access token by checking if it doesn't start with "ory_rt" + const isAccessToken = !token.startsWith("ory_rt"); + // Attempt to validate the access token locally + // If it fails, the token is not active, and we return early + if (isAccessToken) { + try { + await this.validateOAuth2AccessToken({ + token, + requirements: { scopes }, + checkDatabase: false, + userContext, + }); + } catch (error) { + return { active: false }; + } + } + // For tokens that passed local validation or if it's a refresh token, + // validate the token with the database by calling the core introspection endpoint + const res = await querier.sendPostRequest( + new normalisedURLPath_1.default(`/recipe/oauth2/admin/oauth2/introspect`), + { + $isFormData: true, + token, + scope: scopes ? scopes.join(" ") : undefined, + }, + userContext + ); + return res.data; + }, + }; +} +exports.default = getRecipeInterface; diff --git a/lib/build/recipe/oauth2provider/types.d.ts b/lib/build/recipe/oauth2provider/types.d.ts new file mode 100644 index 000000000..7f439c767 --- /dev/null +++ b/lib/build/recipe/oauth2provider/types.d.ts @@ -0,0 +1,464 @@ +// @ts-nocheck +import type { BaseRequest, BaseResponse } from "../../framework"; +import OverrideableBuilder from "supertokens-js-override"; +import { GeneralErrorResponse, JSONObject, JSONValue, NonNullableProperties, UserContext } from "../../types"; +import { SessionContainerInterface } from "../session/types"; +import { OAuth2Client } from "./OAuth2Client"; +import { User } from "../../user"; +export declare type TypeInput = { + override?: { + functions?: ( + originalImplementation: RecipeInterface, + builder?: OverrideableBuilder + ) => RecipeInterface; + apis?: (originalImplementation: APIInterface, builder?: OverrideableBuilder) => APIInterface; + }; +}; +export declare type TypeNormalisedInput = { + override: { + functions: ( + originalImplementation: RecipeInterface, + builder?: OverrideableBuilder + ) => RecipeInterface; + apis: (originalImplementation: APIInterface, builder?: OverrideableBuilder) => APIInterface; + }; +}; +export declare type APIOptions = { + recipeImplementation: RecipeInterface; + config: TypeNormalisedInput; + recipeId: string; + isInServerlessEnv: boolean; + req: BaseRequest; + res: BaseResponse; +}; +export declare type ErrorOAuth2 = { + status: "OAUTH_ERROR"; + error: string; + errorDescription: string; + errorDebug?: string; + errorHint?: string; + statusCode?: number; +}; +export declare type ConsentRequest = { + acr?: string; + amr?: string[]; + challenge: string; + client?: OAuth2Client; + context?: JSONObject; + loginChallenge?: string; + loginSessionId?: string; + oidcContext?: any; + requestedAccessTokenAudience?: string[]; + requestedScope?: string[]; + skip?: boolean; + subject?: string; +}; +export declare type LoginRequest = { + challenge: string; + client: OAuth2Client; + oidcContext?: any; + requestUrl: string; + requestedAccessTokenAudience?: string[]; + requestedScope?: string[]; + sessionId?: string; + skip: boolean; + subject: string; +}; +export declare type TokenInfo = { + access_token?: string; + expires_in: number; + id_token?: string; + refresh_token?: string; + scope: string; + token_type: string; +}; +export declare type LoginInfo = { + clientId: string; + clientName: string; + tosUri?: string; + policyUri?: string; + logoUri?: string; + clientUri?: string; + metadata?: Record | null; +}; +export declare type UserInfo = { + sub: string; + email?: string; + email_verified?: boolean; + phoneNumber?: string; + phoneNumber_verified?: boolean; + [key: string]: JSONValue; +}; +export declare type InstrospectTokenResponse = + | { + active: false; + } + | ({ + active: true; + } & JSONObject); +export declare type RecipeInterface = { + authorization(input: { + params: Record; + cookies: string | undefined; + session: SessionContainerInterface | undefined; + userContext: UserContext; + }): Promise< + | { + status: "OK"; + redirectTo: string; + setCookie: string | undefined; + } + | ErrorOAuth2 + >; + tokenExchange(input: { + authorizationHeader?: string; + body: Record; + userContext: UserContext; + }): Promise; + getConsentRequest(input: { challenge: string; userContext: UserContext }): Promise; + acceptConsentRequest(input: { + challenge: string; + context?: any; + grantAccessTokenAudience?: string[]; + grantScope?: string[]; + handledAt?: string; + remember?: boolean; + rememberFor?: number; + session?: any; + userContext: UserContext; + }): Promise<{ + redirectTo: string; + }>; + rejectConsentRequest(input: { + challenge: string; + error: ErrorOAuth2; + userContext: UserContext; + }): Promise<{ + redirectTo: string; + }>; + getLoginRequest(input: { challenge: string; userContext: UserContext }): Promise; + acceptLoginRequest(input: { + challenge: string; + acr?: string; + amr?: string[]; + context?: any; + extendSessionLifespan?: boolean; + forceSubjectIdentifier?: string; + identityProviderSessionId?: string; + remember?: boolean; + rememberFor?: number; + subject: string; + userContext: UserContext; + }): Promise<{ + redirectTo: string; + }>; + rejectLoginRequest(input: { + challenge: string; + error: ErrorOAuth2; + userContext: UserContext; + }): Promise<{ + redirectTo: string; + }>; + getOAuth2Client(input: { + clientId: string; + userContext: UserContext; + }): Promise< + | { + status: "OK"; + client: OAuth2Client; + } + | { + status: "ERROR"; + error: string; + errorHint: string; + } + >; + getOAuth2Clients( + input: GetOAuth2ClientsInput & { + userContext: UserContext; + } + ): Promise< + | { + status: "OK"; + clients: Array; + nextPaginationToken?: string; + } + | { + status: "ERROR"; + error: string; + errorHint: string; + } + >; + createOAuth2Client( + input: CreateOAuth2ClientInput & { + userContext: UserContext; + } + ): Promise< + | { + status: "OK"; + client: OAuth2Client; + } + | { + status: "ERROR"; + error: string; + errorHint: string; + } + >; + updateOAuth2Client( + input: UpdateOAuth2ClientInput & { + userContext: UserContext; + } + ): Promise< + | { + status: "OK"; + client: OAuth2Client; + } + | { + status: "ERROR"; + error: string; + errorHint: string; + } + >; + deleteOAuth2Client( + input: DeleteOAuth2ClientInput & { + userContext: UserContext; + } + ): Promise< + | { + status: "OK"; + } + | { + status: "ERROR"; + error: string; + errorHint: string; + } + >; + validateOAuth2AccessToken(input: { + token: string; + requirements?: { + clientId?: string; + scopes?: string[]; + audience?: string; + }; + checkDatabase?: boolean; + userContext: UserContext; + }): Promise<{ + status: "OK"; + payload: JSONObject; + }>; + getIssuer(input: { userContext: UserContext }): Promise; + buildAccessTokenPayload(input: { + user: User; + client: OAuth2Client; + sessionHandle: string; + scopes: string[]; + userContext: UserContext; + }): Promise; + buildIdTokenPayload(input: { + user: User; + client: OAuth2Client; + sessionHandle: string; + scopes: string[]; + userContext: UserContext; + }): Promise; + buildUserInfo(input: { + user: User; + accessTokenPayload: JSONObject; + scopes: string[]; + tenantId: string; + userContext: UserContext; + }): Promise; + revokeToken( + input: { + token: string; + userContext: UserContext; + } & ( + | { + authorizationHeader: string; + } + | { + clientId: string; + clientSecret?: string; + } + ) + ): Promise< + | { + status: "OK"; + } + | ErrorOAuth2 + >; + introspectToken(input: { + token: string; + scopes?: string[]; + userContext: UserContext; + }): Promise; +}; +export declare type APIInterface = { + loginGET: + | undefined + | ((input: { + loginChallenge: string; + options: APIOptions; + session?: SessionContainerInterface; + shouldTryRefresh: boolean; + userContext: UserContext; + }) => Promise< + | { + redirectTo: string; + setCookie: string | undefined; + } + | GeneralErrorResponse + >); + authGET: + | undefined + | ((input: { + params: any; + cookie: string | undefined; + session: SessionContainerInterface | undefined; + shouldTryRefresh: boolean; + options: APIOptions; + userContext: UserContext; + }) => Promise< + | { + redirectTo: string; + setCookie: string | undefined; + } + | ErrorOAuth2 + | GeneralErrorResponse + >); + tokenPOST: + | undefined + | ((input: { + authorizationHeader?: string; + body: any; + options: APIOptions; + userContext: UserContext; + }) => Promise); + loginInfoGET: + | undefined + | ((input: { + loginChallenge: string; + options: APIOptions; + userContext: UserContext; + }) => Promise< + | { + status: "OK"; + info: LoginInfo; + } + | GeneralErrorResponse + >); + userInfoGET: + | undefined + | ((input: { + accessTokenPayload: JSONObject; + user: User; + scopes: string[]; + tenantId: string; + options: APIOptions; + userContext: UserContext; + }) => Promise); + revokeTokenPOST: + | undefined + | (( + input: { + token: string; + options: APIOptions; + userContext: UserContext; + } & ( + | { + authorizationHeader: string; + } + | { + clientId: string; + clientSecret?: string; + } + ) + ) => Promise< + | { + status: "OK"; + } + | ErrorOAuth2 + >); + introspectTokenPOST: + | undefined + | ((input: { + token: string; + scopes?: string[]; + options: APIOptions; + userContext: UserContext; + }) => Promise); +}; +export declare type OAuth2ClientOptions = { + clientId: string; + clientSecret?: string; + createdAt: string; + updatedAt: string; + clientName: string; + scope: string; + redirectUris?: string[] | null; + allowedCorsOrigins?: string[]; + authorizationCodeGrantAccessTokenLifespan?: string | null; + authorizationCodeGrantIdTokenLifespan?: string | null; + authorizationCodeGrantRefreshTokenLifespan?: string | null; + clientCredentialsGrantAccessTokenLifespan?: string | null; + implicitGrantAccessTokenLifespan?: string | null; + implicitGrantIdTokenLifespan?: string | null; + refreshTokenGrantAccessTokenLifespan?: string | null; + refreshTokenGrantIdTokenLifespan?: string | null; + refreshTokenGrantRefreshTokenLifespan?: string | null; + tokenEndpointAuthMethod: string; + audience?: string[]; + grantTypes?: string[] | null; + responseTypes?: string[] | null; + clientUri?: string; + logoUri?: string; + policyUri?: string; + tosUri?: string; + metadata?: Record; +}; +export declare type GetOAuth2ClientsInput = { + /** + * Items per Page. Defaults to 250. + */ + pageSize?: number; + /** + * Next Page Token. Defaults to "1". + */ + paginationToken?: string; + /** + * The name of the clients to filter by. + */ + clientName?: string; + /** + * The owner of the clients to filter by. + */ + owner?: string; +}; +export declare type CreateOAuth2ClientInput = Partial< + Omit +>; +export declare type UpdateOAuth2ClientInput = NonNullableProperties< + Omit +> & { + clientId: string; + redirectUris?: string[] | null; + grantTypes?: string[] | null; + responseTypes?: string[] | null; + metadata?: Record | null; +}; +export declare type DeleteOAuth2ClientInput = { + clientId: string; +}; +export declare type PayloadBuilderFunction = ( + user: User, + scopes: string[], + sessionHandle: string, + userContext: UserContext +) => Promise; +export declare type UserInfoBuilderFunction = ( + user: User, + accessTokenPayload: JSONObject, + scopes: string[], + tenantId: string, + userContext: UserContext +) => Promise; diff --git a/lib/build/recipe/oauth2provider/types.js b/lib/build/recipe/oauth2provider/types.js new file mode 100644 index 000000000..a098ca1d7 --- /dev/null +++ b/lib/build/recipe/oauth2provider/types.js @@ -0,0 +1,16 @@ +"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. + */ +Object.defineProperty(exports, "__esModule", { value: true }); diff --git a/lib/build/recipe/oauth2provider/utils.d.ts b/lib/build/recipe/oauth2provider/utils.d.ts new file mode 100644 index 000000000..4025b1b44 --- /dev/null +++ b/lib/build/recipe/oauth2provider/utils.d.ts @@ -0,0 +1,9 @@ +// @ts-nocheck +import { NormalisedAppinfo } from "../../types"; +import Recipe from "./recipe"; +import { TypeInput, TypeNormalisedInput } from "./types"; +export declare function validateAndNormaliseUserInput( + _: Recipe, + __: NormalisedAppinfo, + config?: TypeInput +): TypeNormalisedInput; diff --git a/lib/build/recipe/oauth2provider/utils.js b/lib/build/recipe/oauth2provider/utils.js new file mode 100644 index 000000000..435b4e471 --- /dev/null +++ b/lib/build/recipe/oauth2provider/utils.js @@ -0,0 +1,30 @@ +"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. + */ +Object.defineProperty(exports, "__esModule", { value: true }); +exports.validateAndNormaliseUserInput = void 0; +function validateAndNormaliseUserInput(_, __, config) { + let override = Object.assign( + { + functions: (originalImplementation) => originalImplementation, + apis: (originalImplementation) => originalImplementation, + }, + config === null || config === void 0 ? void 0 : config.override + ); + return { + override, + }; +} +exports.validateAndNormaliseUserInput = validateAndNormaliseUserInput; diff --git a/lib/build/recipe/openid/api/getOpenIdDiscoveryConfiguration.js b/lib/build/recipe/openid/api/getOpenIdDiscoveryConfiguration.js index b308bfffb..025542687 100644 --- a/lib/build/recipe/openid/api/getOpenIdDiscoveryConfiguration.js +++ b/lib/build/recipe/openid/api/getOpenIdDiscoveryConfiguration.js @@ -14,6 +14,14 @@ async function getOpenIdDiscoveryConfiguration(apiImplementation, options, userC utils_1.send200Response(options.res, { issuer: result.issuer, jwks_uri: result.jwks_uri, + authorization_endpoint: result.authorization_endpoint, + token_endpoint: result.token_endpoint, + userinfo_endpoint: result.userinfo_endpoint, + revocation_endpoint: result.revocation_endpoint, + token_introspection_endpoint: result.token_introspection_endpoint, + subject_types_supported: result.subject_types_supported, + id_token_signing_alg_values_supported: result.id_token_signing_alg_values_supported, + response_types_supported: result.response_types_supported, }); } else { utils_1.send200Response(options.res, result); diff --git a/lib/build/recipe/openid/index.d.ts b/lib/build/recipe/openid/index.d.ts index e94fd0092..e32644715 100644 --- a/lib/build/recipe/openid/index.d.ts +++ b/lib/build/recipe/openid/index.d.ts @@ -8,6 +8,14 @@ export default class OpenIdRecipeWrapper { status: "OK"; issuer: string; jwks_uri: string; + authorization_endpoint: string; + token_endpoint: string; + userinfo_endpoint: string; + revocation_endpoint: string; + token_introspection_endpoint: string; + subject_types_supported: string[]; + id_token_signing_alg_values_supported: string[]; + response_types_supported: string[]; }>; static createJWT( payload?: any, diff --git a/lib/build/recipe/openid/recipe.js b/lib/build/recipe/openid/recipe.js index fd7a67398..4e0eb29e1 100644 --- a/lib/build/recipe/openid/recipe.js +++ b/lib/build/recipe/openid/recipe.js @@ -79,7 +79,7 @@ class OpenIdRecipe extends recipeModule_1.default { override: this.config.override.jwtFeature, }); let builder = new supertokens_js_override_1.default( - recipeImplementation_1.default(this.config, this.jwtRecipe.recipeInterfaceImpl) + recipeImplementation_1.default(this.config, this.jwtRecipe.recipeInterfaceImpl, appInfo) ); this.recipeImplementation = builder.override(this.config.override.functions).build(); let apiBuilder = new supertokens_js_override_1.default(implementation_1.default()); diff --git a/lib/build/recipe/openid/recipeImplementation.d.ts b/lib/build/recipe/openid/recipeImplementation.d.ts index d4698099c..be9ecbb29 100644 --- a/lib/build/recipe/openid/recipeImplementation.d.ts +++ b/lib/build/recipe/openid/recipeImplementation.d.ts @@ -1,7 +1,9 @@ // @ts-nocheck import { RecipeInterface, TypeNormalisedInput } from "./types"; import { RecipeInterface as JWTRecipeInterface } from "../jwt/types"; +import { NormalisedAppinfo } from "../../types"; export default function getRecipeInterface( config: TypeNormalisedInput, - jwtRecipeImplementation: JWTRecipeInterface + jwtRecipeImplementation: JWTRecipeInterface, + appInfo: NormalisedAppinfo ): RecipeInterface; diff --git a/lib/build/recipe/openid/recipeImplementation.js b/lib/build/recipe/openid/recipeImplementation.js index 0edb22162..58c278f4f 100644 --- a/lib/build/recipe/openid/recipeImplementation.js +++ b/lib/build/recipe/openid/recipeImplementation.js @@ -7,7 +7,8 @@ var __importDefault = Object.defineProperty(exports, "__esModule", { value: true }); const normalisedURLPath_1 = __importDefault(require("../../normalisedURLPath")); const constants_1 = require("../jwt/constants"); -function getRecipeInterface(config, jwtRecipeImplementation) { +const constants_2 = require("../oauth2provider/constants"); +function getRecipeInterface(config, jwtRecipeImplementation, appInfo) { return { getOpenIdDiscoveryConfiguration: async function () { let issuer = config.issuerDomain.getAsStringDangerous() + config.issuerPath.getAsStringDangerous(); @@ -16,10 +17,19 @@ function getRecipeInterface(config, jwtRecipeImplementation) { config.issuerPath .appendPath(new normalisedURLPath_1.default(constants_1.GET_JWKS_API)) .getAsStringDangerous(); + const apiBasePath = appInfo.apiDomain.getAsStringDangerous() + appInfo.apiBasePath.getAsStringDangerous(); return { status: "OK", issuer, jwks_uri, + authorization_endpoint: apiBasePath + constants_2.AUTH_PATH, + token_endpoint: apiBasePath + constants_2.TOKEN_PATH, + userinfo_endpoint: apiBasePath + constants_2.USER_INFO_PATH, + revocation_endpoint: apiBasePath + constants_2.REVOKE_TOKEN_PATH, + token_introspection_endpoint: apiBasePath + constants_2.INTROSPECT_TOKEN_PATH, + subject_types_supported: ["public"], + id_token_signing_alg_values_supported: ["RS256"], + response_types_supported: ["code", "id_token", "id_token token"], }; }, createJWT: async function ({ payload, validitySeconds, useStaticSigningKey, userContext }) { diff --git a/lib/build/recipe/openid/types.d.ts b/lib/build/recipe/openid/types.d.ts index 5b907a8d5..0908e5b2c 100644 --- a/lib/build/recipe/openid/types.d.ts +++ b/lib/build/recipe/openid/types.d.ts @@ -66,6 +66,14 @@ export declare type APIInterface = { status: "OK"; issuer: string; jwks_uri: string; + authorization_endpoint: string; + token_endpoint: string; + userinfo_endpoint: string; + revocation_endpoint: string; + token_introspection_endpoint: string; + subject_types_supported: string[]; + id_token_signing_alg_values_supported: string[]; + response_types_supported: string[]; } | GeneralErrorResponse >); @@ -77,6 +85,14 @@ export declare type RecipeInterface = { status: "OK"; issuer: string; jwks_uri: string; + authorization_endpoint: string; + token_endpoint: string; + userinfo_endpoint: string; + revocation_endpoint: string; + token_introspection_endpoint: string; + subject_types_supported: string[]; + id_token_signing_alg_values_supported: string[]; + response_types_supported: string[]; }>; createJWT(input: { payload?: any; diff --git a/lib/build/recipe/passwordless/api/consumeCode.js b/lib/build/recipe/passwordless/api/consumeCode.js index fefe0a19b..7179971cf 100644 --- a/lib/build/recipe/passwordless/api/consumeCode.js +++ b/lib/build/recipe/passwordless/api/consumeCode.js @@ -21,7 +21,7 @@ var __importDefault = Object.defineProperty(exports, "__esModule", { value: true }); const utils_1 = require("../../../utils"); const error_1 = __importDefault(require("../error")); -const session_1 = __importDefault(require("../../session")); +const authUtils_1 = require("../../../authUtils"); async function consumeCode(apiImplementation, tenantId, options, userContext) { if (apiImplementation.consumeCodePOST === undefined) { return false; @@ -56,13 +56,11 @@ async function consumeCode(apiImplementation, tenantId, options, userContext) { message: "Please provide one of (linkCode) or (deviceId+userInputCode) and not both", }); } - let session = await session_1.default.getSession( + const shouldTryLinkingWithSessionUser = utils_1.getNormalisedShouldTryLinkingWithSessionUserFlag(options.req, body); + const session = await authUtils_1.AuthUtils.loadSessionInAuthAPIIfNeeded( options.req, options.res, - { - sessionRequired: false, - overrideGlobalClaimValidators: () => [], - }, + shouldTryLinkingWithSessionUser, userContext ); if (session !== undefined) { @@ -76,6 +74,7 @@ async function consumeCode(apiImplementation, tenantId, options, userContext) { preAuthSessionId, tenantId, session, + shouldTryLinkingWithSessionUser, options, userContext, } @@ -85,6 +84,7 @@ async function consumeCode(apiImplementation, tenantId, options, userContext) { preAuthSessionId, tenantId, session, + shouldTryLinkingWithSessionUser, userContext, } ); diff --git a/lib/build/recipe/passwordless/api/createCode.js b/lib/build/recipe/passwordless/api/createCode.js index 06b05ceb5..13617a262 100644 --- a/lib/build/recipe/passwordless/api/createCode.js +++ b/lib/build/recipe/passwordless/api/createCode.js @@ -22,7 +22,7 @@ Object.defineProperty(exports, "__esModule", { value: true }); const utils_1 = require("../../../utils"); const error_1 = __importDefault(require("../error")); const max_1 = __importDefault(require("libphonenumber-js/max")); -const session_1 = __importDefault(require("../../session")); +const authUtils_1 = require("../../../authUtils"); async function createCode(apiImplementation, tenantId, options, userContext) { if (apiImplementation.createCodePOST === undefined) { return false; @@ -84,13 +84,11 @@ async function createCode(apiImplementation, tenantId, options, userContext) { phoneNumber = parsedPhoneNumber.format("E.164"); } } - let session = await session_1.default.getSession( + const shouldTryLinkingWithSessionUser = utils_1.getNormalisedShouldTryLinkingWithSessionUserFlag(options.req, body); + const session = await authUtils_1.AuthUtils.loadSessionInAuthAPIIfNeeded( options.req, options.res, - { - sessionRequired: false, - overrideGlobalClaimValidators: () => [], - }, + shouldTryLinkingWithSessionUser, userContext ); if (session !== undefined) { @@ -98,8 +96,8 @@ async function createCode(apiImplementation, tenantId, options, userContext) { } let result = await apiImplementation.createCodePOST( email !== undefined - ? { email, session, tenantId, options, userContext } - : { phoneNumber: phoneNumber, session, tenantId, options, userContext } + ? { email, session, tenantId, shouldTryLinkingWithSessionUser, options, userContext } + : { phoneNumber: phoneNumber, session, tenantId, shouldTryLinkingWithSessionUser, options, userContext } ); utils_1.send200Response(options.res, result); return true; diff --git a/lib/build/recipe/passwordless/api/implementation.js b/lib/build/recipe/passwordless/api/implementation.js index f0bcdc3ab..0936acc2c 100644 --- a/lib/build/recipe/passwordless/api/implementation.js +++ b/lib/build/recipe/passwordless/api/implementation.js @@ -178,6 +178,7 @@ function getAPIImplementation() { tenantId: input.tenantId, userContext: input.userContext, session: input.session, + shouldTryLinkingWithSessionUser: input.shouldTryLinkingWithSessionUser, }); if (preAuthChecks.status !== "OK") { // On the frontend, this should show a UI of asking the user @@ -203,6 +204,7 @@ function getAPIImplementation() { deviceId: input.deviceId, userInputCode: input.userInputCode, session: input.session, + shouldTryLinkingWithSessionUser: input.shouldTryLinkingWithSessionUser, tenantId: input.tenantId, userContext: input.userContext, } @@ -210,6 +212,7 @@ function getAPIImplementation() { preAuthSessionId: input.preAuthSessionId, linkCode: input.linkCode, session: input.session, + shouldTryLinkingWithSessionUser: input.shouldTryLinkingWithSessionUser, tenantId: input.tenantId, userContext: input.userContext, } @@ -324,6 +327,7 @@ function getAPIImplementation() { factorIds, userContext: input.userContext, session: input.session, + shouldTryLinkingWithSessionUser: input.shouldTryLinkingWithSessionUser, }); if (preAuthChecks.status !== "OK") { // On the frontend, this should show a UI of asking the user @@ -347,6 +351,7 @@ function getAPIImplementation() { input.userContext ), session: input.session, + shouldTryLinkingWithSessionUser: input.shouldTryLinkingWithSessionUser, tenantId: input.tenantId, } : { @@ -360,11 +365,16 @@ function getAPIImplementation() { input.userContext ), session: input.session, + shouldTryLinkingWithSessionUser: input.shouldTryLinkingWithSessionUser, tenantId: input.tenantId, } ); if (response.status !== "OK") { - return authUtils_1.AuthUtils.getErrorStatusResponseWithReason(response, {}, "SIGN_IN_UP_NOT_ALLOWED"); + return authUtils_1.AuthUtils.getErrorStatusResponseWithReason( + response, + errorCodeMap, + "SIGN_IN_UP_NOT_ALLOWED" + ); } // now we send the email / text message. let magicLink = undefined; @@ -493,6 +503,7 @@ function getAPIImplementation() { ); const authTypeInfo = await authUtils_1.AuthUtils.checkAuthTypeAndLinkingStatus( input.session, + input.shouldTryLinkingWithSessionUser, { recipeId: "passwordless", email: deviceInfo.email, @@ -543,7 +554,7 @@ function getAPIImplementation() { let userInputCode = undefined; // This mirrors how we construct factorIds in createCodePOST let factorIds; - if (input.session !== undefined) { + if (!authTypeInfo.isFirstFactor) { if (deviceInfo.email !== undefined) { factorIds = [multifactorauth_1.FactorIds.OTP_EMAIL]; } else { diff --git a/lib/build/recipe/passwordless/api/resendCode.js b/lib/build/recipe/passwordless/api/resendCode.js index 419a905ac..131c04f89 100644 --- a/lib/build/recipe/passwordless/api/resendCode.js +++ b/lib/build/recipe/passwordless/api/resendCode.js @@ -21,7 +21,7 @@ var __importDefault = Object.defineProperty(exports, "__esModule", { value: true }); const utils_1 = require("../../../utils"); const error_1 = __importDefault(require("../error")); -const session_1 = __importDefault(require("../../session")); +const authUtils_1 = require("../../../authUtils"); async function resendCode(apiImplementation, tenantId, options, userContext) { if (apiImplementation.resendCodePOST === undefined) { return false; @@ -41,23 +41,19 @@ async function resendCode(apiImplementation, tenantId, options, userContext) { message: "Please provide a deviceId", }); } - let session = await session_1.default.getSession( + const shouldTryLinkingWithSessionUser = utils_1.getNormalisedShouldTryLinkingWithSessionUserFlag(options.req, body); + const session = await authUtils_1.AuthUtils.loadSessionInAuthAPIIfNeeded( options.req, options.res, - { - sessionRequired: false, - overrideGlobalClaimValidators: () => [], - }, + shouldTryLinkingWithSessionUser, userContext ); - if (session !== undefined) { - tenantId = session.getTenantId(); - } let result = await apiImplementation.resendCodePOST({ deviceId, preAuthSessionId, tenantId, session, + shouldTryLinkingWithSessionUser, options, userContext, }); diff --git a/lib/build/recipe/passwordless/index.js b/lib/build/recipe/passwordless/index.js index 74774d368..285ccb9ed 100644 --- a/lib/build/recipe/passwordless/index.js +++ b/lib/build/recipe/passwordless/index.js @@ -29,6 +29,7 @@ class Wrapper { return recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.createCode( Object.assign(Object.assign({}, input), { session: input.session, + shouldTryLinkingWithSessionUser: !!input.session, userContext: utils_1.getUserContext(input.userContext), }) ); @@ -44,6 +45,7 @@ class Wrapper { return recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.consumeCode( Object.assign(Object.assign({}, input), { session: input.session, + shouldTryLinkingWithSessionUser: !!input.session, userContext: utils_1.getUserContext(input.userContext), }) ); diff --git a/lib/build/recipe/passwordless/recipe.js b/lib/build/recipe/passwordless/recipe.js index a18ca5fc2..641ed598b 100644 --- a/lib/build/recipe/passwordless/recipe.js +++ b/lib/build/recipe/passwordless/recipe.js @@ -140,6 +140,7 @@ class Recipe extends recipeModule_1.default { email: input.email, userInputCode, session: input.session, + shouldTryLinkingWithSessionUser: !!input.session, tenantId: input.tenantId, userContext: input.userContext, } @@ -147,6 +148,7 @@ class Recipe extends recipeModule_1.default { phoneNumber: input.phoneNumber, userInputCode, session: input.session, + shouldTryLinkingWithSessionUser: !!input.session, tenantId: input.tenantId, userContext: input.userContext, } @@ -179,12 +181,14 @@ class Recipe extends recipeModule_1.default { email: input.email, tenantId: input.tenantId, session: input.session, + shouldTryLinkingWithSessionUser: !!input.session, userContext: input.userContext, } : { phoneNumber: input.phoneNumber, tenantId: input.tenantId, session: input.session, + shouldTryLinkingWithSessionUser: !!input.session, userContext: input.userContext, } ); @@ -197,6 +201,7 @@ class Recipe extends recipeModule_1.default { preAuthSessionId: codeInfo.preAuthSessionId, linkCode: codeInfo.linkCode, session: input.session, + shouldTryLinkingWithSessionUser: !!input.session, tenantId: input.tenantId, userContext: input.userContext, } @@ -205,6 +210,7 @@ class Recipe extends recipeModule_1.default { deviceId: codeInfo.deviceId, userInputCode: codeInfo.userInputCode, session: input.session, + shouldTryLinkingWithSessionUser: !!input.session, tenantId: input.tenantId, userContext: input.userContext, } diff --git a/lib/build/recipe/passwordless/recipeImplementation.js b/lib/build/recipe/passwordless/recipeImplementation.js index 15e4e9238..2ec207ff3 100644 --- a/lib/build/recipe/passwordless/recipeImplementation.js +++ b/lib/build/recipe/passwordless/recipeImplementation.js @@ -45,12 +45,13 @@ function getRecipeInterface(querier) { } // Attempt account linking (this is a sign up) let updatedUser = response.user; - const linkResult = await authUtils_1.AuthUtils.linkToSessionIfProvidedElseCreatePrimaryUserIdOrLinkByAccountInfo( + const linkResult = await authUtils_1.AuthUtils.linkToSessionIfRequiredElseCreatePrimaryUserIdOrLinkByAccountInfo( { tenantId: input.tenantId, inputUser: response.user, recipeUserId: response.recipeUserId, session: input.session, + shouldTryLinkingWithSessionUser: input.shouldTryLinkingWithSessionUser, userContext: input.userContext, } ); @@ -183,6 +184,7 @@ function getRecipeInterface(querier) { let response = await querier.sendPutRequest( new normalisedURLPath_1.default(`/recipe/user`), copyAndRemoveUserContextAndTenantId(input), + {}, input.userContext ); if (response.status !== "OK") { diff --git a/lib/build/recipe/passwordless/types.d.ts b/lib/build/recipe/passwordless/types.d.ts index 2535a78f6..434f7902e 100644 --- a/lib/build/recipe/passwordless/types.d.ts +++ b/lib/build/recipe/passwordless/types.d.ts @@ -92,6 +92,7 @@ export declare type RecipeInterface = { ) & { userInputCode?: string; session: SessionContainerInterface | undefined; + shouldTryLinkingWithSessionUser: boolean | undefined; tenantId: string; userContext: UserContext; } @@ -132,6 +133,7 @@ export declare type RecipeInterface = { deviceId: string; preAuthSessionId: string; session: SessionContainerInterface | undefined; + shouldTryLinkingWithSessionUser: boolean | undefined; tenantId: string; userContext: UserContext; } @@ -139,6 +141,7 @@ export declare type RecipeInterface = { linkCode: string; preAuthSessionId: string; session: SessionContainerInterface | undefined; + shouldTryLinkingWithSessionUser: boolean | undefined; tenantId: string; userContext: UserContext; } @@ -305,6 +308,7 @@ export declare type APIInterface = { ) & { tenantId: string; session: SessionContainerInterface | undefined; + shouldTryLinkingWithSessionUser: boolean | undefined; options: APIOptions; userContext: UserContext; } @@ -328,6 +332,7 @@ export declare type APIInterface = { } & { tenantId: string; session: SessionContainerInterface | undefined; + shouldTryLinkingWithSessionUser: boolean | undefined; options: APIOptions; userContext: UserContext; } @@ -351,6 +356,7 @@ export declare type APIInterface = { ) & { tenantId: string; session: SessionContainerInterface | undefined; + shouldTryLinkingWithSessionUser: boolean | undefined; options: APIOptions; userContext: UserContext; } diff --git a/lib/build/recipe/session/accessToken.js b/lib/build/recipe/session/accessToken.js index 3f0efd2cf..37249db05 100644 --- a/lib/build/recipe/session/accessToken.js +++ b/lib/build/recipe/session/accessToken.js @@ -199,6 +199,9 @@ async function getInfoFromAccessToken(jwtInfo, jwks, doAntiCsrfCheck) { } exports.getInfoFromAccessToken = getInfoFromAccessToken; function validateAccessTokenStructure(payload, version) { + if (payload.stt !== 0 && payload.stt !== undefined) { + throw Error("Wrong token type"); + } if (version >= 5) { if ( typeof payload.sub !== "string" || diff --git a/lib/build/recipe/session/constants.js b/lib/build/recipe/session/constants.js index 4aa6e6b05..84d5b0225 100644 --- a/lib/build/recipe/session/constants.js +++ b/lib/build/recipe/session/constants.js @@ -30,4 +30,5 @@ exports.protectedProps = [ "antiCsrfToken", "rsub", "tId", + "stt", ]; diff --git a/lib/build/recipe/session/index.d.ts b/lib/build/recipe/session/index.d.ts index 55fbe988b..fb87a07d1 100644 --- a/lib/build/recipe/session/index.d.ts +++ b/lib/build/recipe/session/index.d.ts @@ -177,6 +177,14 @@ export default class SessionWrapper { status: "OK"; issuer: string; jwks_uri: string; + authorization_endpoint: string; + token_endpoint: string; + userinfo_endpoint: string; + revocation_endpoint: string; + token_introspection_endpoint: string; + subject_types_supported: string[]; + id_token_signing_alg_values_supported: string[]; + response_types_supported: string[]; }>; static fetchAndSetClaim( sessionHandle: string, diff --git a/lib/build/recipe/session/recipe.d.ts b/lib/build/recipe/session/recipe.d.ts index 0489b1e81..d6259116f 100644 --- a/lib/build/recipe/session/recipe.d.ts +++ b/lib/build/recipe/session/recipe.d.ts @@ -56,4 +56,12 @@ export default class SessionRecipe extends RecipeModule { response: BaseResponse, userContext: UserContext ) => Promise; + getAccessTokenFromRequest: ( + req: any, + userContext: UserContext + ) => { + requestTransferMethod: import("./types").TokenTransferMethod | undefined; + accessToken: import("./jwt").ParsedJWTInfo | undefined; + allowedTransferMethod: import("./types").TokenTransferMethod | "any"; + }; } diff --git a/lib/build/recipe/session/recipe.js b/lib/build/recipe/session/recipe.js index 77afe511a..b7307ae08 100644 --- a/lib/build/recipe/session/recipe.js +++ b/lib/build/recipe/session/recipe.js @@ -33,6 +33,8 @@ const implementation_1 = __importDefault(require("./api/implementation")); const supertokens_js_override_1 = __importDefault(require("supertokens-js-override")); const recipe_1 = __importDefault(require("../openid/recipe")); const logger_1 = require("../../logger"); +const combinedRemoteJWKSet_1 = require("../../combinedRemoteJWKSet"); +const sessionRequestFunctions_1 = require("./sessionRequestFunctions"); // For Express class SessionRecipe extends recipeModule_1.default { constructor(recipeId, appInfo, isInServerlessEnv, config) { @@ -182,6 +184,14 @@ class SessionRecipe extends recipeModule_1.default { userContext, }); }; + this.getAccessTokenFromRequest = (req, userContext) => { + const allowedTransferMethod = this.config.getTokenTransferMethod({ + req, + forCreateNewSession: false, + userContext, + }); + return sessionRequestFunctions_1.getAccessTokenFromRequest(req, allowedTransferMethod); + }; this.config = utils_1.validateAndNormaliseUserInput(this, appInfo, config); const antiCsrfToLog = typeof this.config.antiCsrfFunctionOrString === "string" @@ -238,6 +248,7 @@ class SessionRecipe extends recipeModule_1.default { throw new Error("calling testing function in non testing env"); } SessionRecipe.instance = undefined; + combinedRemoteJWKSet_1.resetCombinedJWKS(); } } exports.default = SessionRecipe; diff --git a/lib/build/recipe/session/recipeImplementation.d.ts b/lib/build/recipe/session/recipeImplementation.d.ts index 9c1969e84..df95edba8 100644 --- a/lib/build/recipe/session/recipeImplementation.d.ts +++ b/lib/build/recipe/session/recipeImplementation.d.ts @@ -1,11 +1,9 @@ // @ts-nocheck -import { JWTVerifyGetKey } from "jose"; import { RecipeInterface, TypeNormalisedInput } from "./types"; import { Querier } from "../../querier"; import { NormalisedAppinfo } from "../../types"; export declare type Helpers = { querier: Querier; - JWKS: JWTVerifyGetKey; config: TypeNormalisedInput; appInfo: NormalisedAppinfo; getRecipeImpl: () => RecipeInterface; diff --git a/lib/build/recipe/session/recipeImplementation.js b/lib/build/recipe/session/recipeImplementation.js index 39c601791..143db8845 100644 --- a/lib/build/recipe/session/recipeImplementation.js +++ b/lib/build/recipe/session/recipeImplementation.js @@ -41,7 +41,6 @@ var __importDefault = return mod && mod.__esModule ? mod : { default: mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); -const jose_1 = require("jose"); const SessionFunctions = __importStar(require("./sessionFunctions")); const cookieAndHeaders_1 = require("./cookieAndHeaders"); const utils_1 = require("./utils"); @@ -55,36 +54,6 @@ const recipeUserId_1 = __importDefault(require("../../recipeUserId")); const constants_1 = require("../multitenancy/constants"); const constants_2 = require("./constants"); function getRecipeInterface(querier, config, appInfo, getRecipeImplAfterOverrides) { - const JWKS = querier.getAllCoreUrlsForPath("/.well-known/jwks.json").map((url) => - jose_1.createRemoteJWKSet(new URL(url), { - cooldownDuration: constants_2.JWKCacheCooldownInMs, - cacheMaxAge: config.jwksRefreshIntervalSec * 1000, - }) - ); - /** - This function fetches all JWKs from the first available core instance. This combines the other JWKS functions to become - error resistant. - - Every core instance a backend is connected to is expected to connect to the same database and use the same key set for - token verification. Otherwise, the result of session verification would depend on which core is currently available. - */ - const combinedJWKS = async (...args) => { - let lastError = undefined; - if (JWKS.length === 0) { - throw Error( - "No SuperTokens core available to query. Please pass supertokens > connectionURI to the init function, or override all the functions of the recipe you are using." - ); - } - for (const jwks of JWKS) { - try { - // We await before returning to make sure we catch the error - return await jwks(...args); - } catch (ex) { - lastError = ex; - } - } - throw lastError; - }; let obj = { createNewSession: async function ({ recipeUserId, @@ -437,7 +406,6 @@ function getRecipeInterface(querier, config, appInfo, getRecipeImplAfterOverride }; let helpers = { querier, - JWKS: combinedJWKS, config, appInfo, getRecipeImpl: getRecipeImplAfterOverrides, diff --git a/lib/build/recipe/session/sessionFunctions.js b/lib/build/recipe/session/sessionFunctions.js index 2e522a2c8..750831f0a 100644 --- a/lib/build/recipe/session/sessionFunctions.js +++ b/lib/build/recipe/session/sessionFunctions.js @@ -28,6 +28,7 @@ const utils_1 = require("../../utils"); const logger_1 = require("../../logger"); const recipeUserId_1 = __importDefault(require("../../recipeUserId")); const constants_1 = require("../multitenancy/constants"); +const combinedRemoteJWKSet_1 = require("../../combinedRemoteJWKSet"); /** * @description call this to "login" a user. */ @@ -98,7 +99,7 @@ async function getSession( */ accessTokenInfo = await accessToken_1.getInfoFromAccessToken( parsedAccessToken, - helpers.JWKS, + combinedRemoteJWKSet_1.getCombinedJWKS(), helpers.config.antiCsrfFunctionOrString === "VIA_TOKEN" && doAntiCsrfCheck ); } catch (err) { @@ -460,6 +461,7 @@ async function updateSessionDataInDatabase(helpers, sessionHandle, newSessionDat sessionHandle, userDataInDatabase: newSessionData, }, + {}, userContext ); if (response.status === "UNAUTHORISED") { @@ -477,6 +479,7 @@ async function updateAccessTokenPayload(helpers, sessionHandle, newAccessTokenPa sessionHandle, userDataInJWT: newAccessTokenPayload, }, + {}, userContext ); if (response.status === "UNAUTHORISED") { diff --git a/lib/build/recipe/session/sessionRequestFunctions.d.ts b/lib/build/recipe/session/sessionRequestFunctions.d.ts index 7204931a1..3002f3bd2 100644 --- a/lib/build/recipe/session/sessionRequestFunctions.d.ts +++ b/lib/build/recipe/session/sessionRequestFunctions.d.ts @@ -1,6 +1,13 @@ // @ts-nocheck import Recipe from "./recipe"; -import { VerifySessionOptions, RecipeInterface, TypeNormalisedInput, SessionContainerInterface } from "./types"; +import { + VerifySessionOptions, + RecipeInterface, + TokenTransferMethod, + TypeNormalisedInput, + SessionContainerInterface, +} from "./types"; +import { ParsedJWTInfo } from "./jwt"; import { NormalisedAppinfo, UserContext } from "../../types"; import RecipeUserId from "../../recipeUserId"; export declare function getSessionFromRequest({ @@ -18,6 +25,14 @@ export declare function getSessionFromRequest({ options?: VerifySessionOptions; userContext: UserContext; }): Promise; +export declare function getAccessTokenFromRequest( + req: any, + allowedTransferMethod: TokenTransferMethod | "any" +): { + requestTransferMethod: TokenTransferMethod | undefined; + accessToken: ParsedJWTInfo | undefined; + allowedTransferMethod: TokenTransferMethod | "any"; +}; export declare function refreshSessionInRequest({ res, req, diff --git a/lib/build/recipe/session/sessionRequestFunctions.js b/lib/build/recipe/session/sessionRequestFunctions.js index 1d33897a7..ea6c2819f 100644 --- a/lib/build/recipe/session/sessionRequestFunctions.js +++ b/lib/build/recipe/session/sessionRequestFunctions.js @@ -5,7 +5,7 @@ var __importDefault = return mod && mod.__esModule ? mod : { default: mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); -exports.createNewSessionInRequest = exports.refreshSessionInRequest = exports.getSessionFromRequest = void 0; +exports.createNewSessionInRequest = exports.refreshSessionInRequest = exports.getAccessTokenFromRequest = exports.getSessionFromRequest = void 0; const framework_1 = __importDefault(require("../../framework")); const supertokens_1 = __importDefault(require("../../supertokens")); const utils_1 = require("./utils"); @@ -44,58 +44,12 @@ async function getSessionFromRequest({ req, res, config, recipeInterfaceImpl, op } const sessionOptional = (options === null || options === void 0 ? void 0 : options.sessionRequired) === false; logger_1.logDebugMessage("getSession: optional validation: " + sessionOptional); - const accessTokens = {}; - // We check all token transfer methods for available access tokens - for (const transferMethod of constants_1.availableTokenTransferMethods) { - const tokenString = cookieAndHeaders_1.getToken(req, "access", transferMethod); - if (tokenString !== undefined) { - try { - const info = jwt_1.parseJWTWithoutSignatureVerification(tokenString); - accessToken_1.validateAccessTokenStructure(info.payload, info.version); - logger_1.logDebugMessage("getSession: got access token from " + transferMethod); - accessTokens[transferMethod] = info; - } catch (_a) { - logger_1.logDebugMessage( - `getSession: ignoring token in ${transferMethod}, because it doesn't match our access token structure` - ); - } - } - } const allowedTransferMethod = config.getTokenTransferMethod({ req, forCreateNewSession: false, userContext, }); - let requestTransferMethod; - let accessToken; - if ( - (allowedTransferMethod === "any" || allowedTransferMethod === "header") && - accessTokens["header"] !== undefined - ) { - logger_1.logDebugMessage("getSession: using header transfer method"); - requestTransferMethod = "header"; - accessToken = accessTokens["header"]; - } else if ( - (allowedTransferMethod === "any" || allowedTransferMethod === "cookie") && - accessTokens["cookie"] !== undefined - ) { - logger_1.logDebugMessage("getSession: using cookie transfer method"); - // If multiple access tokens exist in the request cookie, throw TRY_REFRESH_TOKEN. - // This prompts the client to call the refresh endpoint, clearing olderCookieDomain cookies (if set). - // ensuring outdated token payload isn't used. - const hasMultipleAccessTokenCookies = cookieAndHeaders_1.hasMultipleCookiesForTokenType(req, "access"); - if (hasMultipleAccessTokenCookies) { - logger_1.logDebugMessage( - "getSession: Throwing TRY_REFRESH_TOKEN because multiple access tokens are present in request cookies" - ); - throw new error_1.default({ - message: "Multiple access tokens present in the request cookies.", - type: error_1.default.TRY_REFRESH_TOKEN, - }); - } - requestTransferMethod = "cookie"; - accessToken = accessTokens["cookie"]; - } + const { requestTransferMethod, accessToken } = getAccessTokenFromRequest(req, allowedTransferMethod); let antiCsrfToken = cookieAndHeaders_1.getAntiCsrfTokenFromHeaders(req); let doAntiCsrfCheck = options !== undefined ? options.antiCsrfCheck : undefined; if (doAntiCsrfCheck === undefined) { @@ -168,6 +122,57 @@ async function getSessionFromRequest({ req, res, config, recipeInterfaceImpl, op return session; } exports.getSessionFromRequest = getSessionFromRequest; +function getAccessTokenFromRequest(req, allowedTransferMethod) { + const accessTokens = {}; + // We check all token transfer methods for available access tokens + for (const transferMethod of constants_1.availableTokenTransferMethods) { + const tokenString = cookieAndHeaders_1.getToken(req, "access", transferMethod); + if (tokenString !== undefined) { + try { + const info = jwt_1.parseJWTWithoutSignatureVerification(tokenString); + accessToken_1.validateAccessTokenStructure(info.payload, info.version); + logger_1.logDebugMessage("getSession: got access token from " + transferMethod); + accessTokens[transferMethod] = info; + } catch (_a) { + logger_1.logDebugMessage( + `getSession: ignoring token in ${transferMethod}, because it doesn't match our access token structure` + ); + } + } + } + let requestTransferMethod; + let accessToken; + if ( + (allowedTransferMethod === "any" || allowedTransferMethod === "header") && + accessTokens["header"] !== undefined + ) { + logger_1.logDebugMessage("getSession: using header transfer method"); + requestTransferMethod = "header"; + accessToken = accessTokens["header"]; + } else if ( + (allowedTransferMethod === "any" || allowedTransferMethod === "cookie") && + accessTokens["cookie"] !== undefined + ) { + logger_1.logDebugMessage("getSession: using cookie transfer method"); + // If multiple access tokens exist in the request cookie, throw TRY_REFRESH_TOKEN. + // This prompts the client to call the refresh endpoint, clearing olderCookieDomain cookies (if set). + // ensuring outdated token payload isn't used. + const hasMultipleAccessTokenCookies = cookieAndHeaders_1.hasMultipleCookiesForTokenType(req, "access"); + if (hasMultipleAccessTokenCookies) { + logger_1.logDebugMessage( + "getSession: Throwing TRY_REFRESH_TOKEN because multiple access tokens are present in request cookies" + ); + throw new error_1.default({ + message: "Multiple access tokens present in the request cookies.", + type: error_1.default.TRY_REFRESH_TOKEN, + }); + } + requestTransferMethod = "cookie"; + accessToken = accessTokens["cookie"]; + } + return { requestTransferMethod, accessToken, allowedTransferMethod }; +} +exports.getAccessTokenFromRequest = getAccessTokenFromRequest; /* In all cases: if sIdRefreshToken token exists (so it's a legacy session) we clear it. Check http://localhost:3002/docs/contribute/decisions/session/0008 for further details and a table of expected behaviours diff --git a/lib/build/recipe/session/utils.js b/lib/build/recipe/session/utils.js index e8f0af0b1..5011474a8 100644 --- a/lib/build/recipe/session/utils.js +++ b/lib/build/recipe/session/utils.js @@ -234,7 +234,7 @@ function validateAndNormaliseUserInput(recipeInstance, appInfo, config) { (_d = config === null || config === void 0 ? void 0 : config.overwriteSessionDuringSignInUp) !== null && _d !== void 0 ? _d - : false, + : true, jwksRefreshIntervalSec: (_e = config === null || config === void 0 ? void 0 : config.jwksRefreshIntervalSec) !== null && _e !== void 0 diff --git a/lib/build/recipe/thirdparty/api/implementation.js b/lib/build/recipe/thirdparty/api/implementation.js index e688b95ae..0188b26dd 100644 --- a/lib/build/recipe/thirdparty/api/implementation.js +++ b/lib/build/recipe/thirdparty/api/implementation.js @@ -128,6 +128,7 @@ function getAPIInterface() { tenantId: input.tenantId, userContext: input.userContext, session: input.session, + shouldTryLinkingWithSessionUser: input.shouldTryLinkingWithSessionUser, }); if (preAuthChecks.status !== "OK") { logger_1.logDebugMessage( @@ -149,6 +150,7 @@ function getAPIInterface() { oAuthTokens: oAuthTokensToUse, rawUserInfoFromProvider: userInfo.rawUserInfoFromProvider, session: input.session, + shouldTryLinkingWithSessionUser: input.shouldTryLinkingWithSessionUser, tenantId, userContext, }); diff --git a/lib/build/recipe/thirdparty/api/signinup.js b/lib/build/recipe/thirdparty/api/signinup.js index 027fcc4d7..772fd4bcd 100644 --- a/lib/build/recipe/thirdparty/api/signinup.js +++ b/lib/build/recipe/thirdparty/api/signinup.js @@ -21,7 +21,7 @@ var __importDefault = Object.defineProperty(exports, "__esModule", { value: true }); const error_1 = __importDefault(require("../error")); const utils_1 = require("../../../utils"); -const session_1 = __importDefault(require("../../session")); +const authUtils_1 = require("../../../authUtils"); async function signInUpAPI(apiImplementation, tenantId, options, userContext) { if (apiImplementation.signInUpPOST === undefined) { return false; @@ -66,13 +66,14 @@ async function signInUpAPI(apiImplementation, tenantId, options, userContext) { }); } const provider = providerResponse; - let session = await session_1.default.getSession( + const shouldTryLinkingWithSessionUser = utils_1.getNormalisedShouldTryLinkingWithSessionUserFlag( + options.req, + bodyParams + ); + const session = await authUtils_1.AuthUtils.loadSessionInAuthAPIIfNeeded( options.req, options.res, - { - sessionRequired: false, - overrideGlobalClaimValidators: () => [], - }, + shouldTryLinkingWithSessionUser, userContext ); if (session !== undefined) { @@ -84,6 +85,7 @@ async function signInUpAPI(apiImplementation, tenantId, options, userContext) { oAuthTokens, tenantId, session, + shouldTryLinkingWithSessionUser, options, userContext, }); diff --git a/lib/build/recipe/thirdparty/index.js b/lib/build/recipe/thirdparty/index.js index 6e8aadbe4..2ee060ce9 100644 --- a/lib/build/recipe/thirdparty/index.js +++ b/lib/build/recipe/thirdparty/index.js @@ -49,6 +49,7 @@ class Wrapper { tenantId: tenantId === undefined ? constants_1.DEFAULT_TENANT_ID : tenantId, isVerified, session, + shouldTryLinkingWithSessionUser: !!session, userContext: utils_1.getUserContext(userContext), }); } diff --git a/lib/build/recipe/thirdparty/providers/bitbucket.js b/lib/build/recipe/thirdparty/providers/bitbucket.js index 27588fc51..6c5a60d23 100644 --- a/lib/build/recipe/thirdparty/providers/bitbucket.js +++ b/lib/build/recipe/thirdparty/providers/bitbucket.js @@ -19,7 +19,7 @@ var __importDefault = return mod && mod.__esModule ? mod : { default: mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); -const utils_1 = require("./utils"); +const thirdpartyUtils_1 = require("../../../thirdpartyUtils"); const custom_1 = __importDefault(require("./custom")); const logger_1 = require("../../../logger"); function Bitbucket(input) { @@ -59,7 +59,7 @@ function Bitbucket(input) { fromUserInfoAPI: {}, fromIdTokenPayload: {}, }; - const userInfoFromAccessToken = await utils_1.doGetRequest( + const userInfoFromAccessToken = await thirdpartyUtils_1.doGetRequest( "https://api.bitbucket.org/2.0/user", undefined, headers @@ -73,7 +73,7 @@ function Bitbucket(input) { ); } rawUserInfoFromProvider.fromUserInfoAPI = userInfoFromAccessToken.jsonResponse; - const userInfoFromEmail = await utils_1.doGetRequest( + const userInfoFromEmail = await thirdpartyUtils_1.doGetRequest( "https://api.bitbucket.org/2.0/user/emails", undefined, headers diff --git a/lib/build/recipe/thirdparty/providers/custom.js b/lib/build/recipe/thirdparty/providers/custom.js index 3aead6ffc..ddd2b7a00 100644 --- a/lib/build/recipe/thirdparty/providers/custom.js +++ b/lib/build/recipe/thirdparty/providers/custom.js @@ -6,7 +6,7 @@ var __importDefault = }; Object.defineProperty(exports, "__esModule", { value: true }); exports.getActualClientIdFromDevelopmentClientId = exports.isUsingDevelopmentClientId = exports.DEV_OAUTH_REDIRECT_URL = void 0; -const utils_1 = require("./utils"); +const thirdpartyUtils_1 = require("../../../thirdpartyUtils"); const pkce_challenge_1 = __importDefault(require("pkce-challenge")); const configUtils_1 = require("./configUtils"); const jose_1 = require("jose"); @@ -251,7 +251,7 @@ function NewProvider(input) { accessTokenAPIParams["redirect_uri"] = exports.DEV_OAUTH_REDIRECT_URL; } /* Transformation needed for dev keys END */ - const tokenResponse = await utils_1.doPostRequest(tokenAPIURL, accessTokenAPIParams); + const tokenResponse = await thirdpartyUtils_1.doPostRequest(tokenAPIURL, accessTokenAPIParams); if (tokenResponse.status >= 400) { logger_1.logDebugMessage( `Received response with status ${tokenResponse.status} and body ${tokenResponse.stringResponse}` @@ -273,7 +273,7 @@ function NewProvider(input) { if (jwks === undefined) { jwks = jose_1.createRemoteJWKSet(new URL(impl.config.jwksURI)); } - rawUserInfoFromProvider.fromIdTokenPayload = await utils_1.verifyIdTokenFromJWKSEndpointAndGetPayload( + rawUserInfoFromProvider.fromIdTokenPayload = await thirdpartyUtils_1.verifyIdTokenFromJWKSEndpointAndGetPayload( idToken, jwks, { @@ -318,7 +318,7 @@ function NewProvider(input) { } } } - const userInfoFromAccessToken = await utils_1.doGetRequest( + const userInfoFromAccessToken = await thirdpartyUtils_1.doGetRequest( impl.config.userInfoEndpoint, queryParams, headers diff --git a/lib/build/recipe/thirdparty/providers/github.js b/lib/build/recipe/thirdparty/providers/github.js index a656dd895..1f3c86029 100644 --- a/lib/build/recipe/thirdparty/providers/github.js +++ b/lib/build/recipe/thirdparty/providers/github.js @@ -6,7 +6,7 @@ var __importDefault = }; Object.defineProperty(exports, "__esModule", { value: true }); const custom_1 = __importDefault(require("./custom")); -const utils_1 = require("./utils"); +const thirdpartyUtils_1 = require("../../../thirdpartyUtils"); function getSupertokensUserInfoFromRawUserInfoResponseForGithub(rawUserInfoResponse) { if (rawUserInfoResponse.fromUserInfoAPI === undefined) { throw new Error("rawUserInfoResponse.fromUserInfoAPI is not available"); @@ -44,7 +44,7 @@ function Github(input) { const basicAuthToken = Buffer.from( `${clientConfig.clientId}:${clientConfig.clientSecret === undefined ? "" : clientConfig.clientSecret}` ).toString("base64"); - const applicationResponse = await utils_1.doPostRequest( + const applicationResponse = await thirdpartyUtils_1.doPostRequest( `https://api.github.com/applications/${clientConfig.clientId}/token`, { access_token: accessToken, @@ -81,14 +81,22 @@ function Github(input) { Accept: "application/vnd.github.v3+json", }; const rawResponse = {}; - const emailInfoResp = await utils_1.doGetRequest("https://api.github.com/user/emails", undefined, headers); + const emailInfoResp = await thirdpartyUtils_1.doGetRequest( + "https://api.github.com/user/emails", + undefined, + headers + ); if (emailInfoResp.status >= 400) { throw new Error( `Getting userInfo failed with ${emailInfoResp.status}: ${emailInfoResp.stringResponse}` ); } rawResponse.emails = emailInfoResp.jsonResponse; - const userInfoResp = await utils_1.doGetRequest("https://api.github.com/user", undefined, headers); + const userInfoResp = await thirdpartyUtils_1.doGetRequest( + "https://api.github.com/user", + undefined, + headers + ); if (userInfoResp.status >= 400) { throw new Error(`Getting userInfo failed with ${userInfoResp.status}: ${userInfoResp.stringResponse}`); } diff --git a/lib/build/recipe/thirdparty/providers/linkedin.js b/lib/build/recipe/thirdparty/providers/linkedin.js index bce0eeaf4..defa0739c 100644 --- a/lib/build/recipe/thirdparty/providers/linkedin.js +++ b/lib/build/recipe/thirdparty/providers/linkedin.js @@ -21,7 +21,7 @@ Object.defineProperty(exports, "__esModule", { value: true }); */ const logger_1 = require("../../../logger"); const custom_1 = __importDefault(require("./custom")); -const utils_1 = require("./utils"); +const thirdpartyUtils_1 = require("../../../thirdpartyUtils"); function Linkedin(input) { if (input.config.name === undefined) { input.config.name = "LinkedIn"; @@ -56,7 +56,7 @@ function Linkedin(input) { fromIdTokenPayload: {}, }; // https://learn.microsoft.com/en-us/linkedin/consumer/integrations/self-serve/sign-in-with-linkedin-v2?context=linkedin%2Fconsumer%2Fcontext#sample-api-response - const userInfoFromAccessToken = await utils_1.doGetRequest( + const userInfoFromAccessToken = await thirdpartyUtils_1.doGetRequest( "https://api.linkedin.com/v2/userinfo", undefined, headers diff --git a/lib/build/recipe/thirdparty/providers/twitter.js b/lib/build/recipe/thirdparty/providers/twitter.js index 3e54592c7..7a7078b51 100644 --- a/lib/build/recipe/thirdparty/providers/twitter.js +++ b/lib/build/recipe/thirdparty/providers/twitter.js @@ -52,7 +52,7 @@ Object.defineProperty(exports, "__esModule", { value: true }); */ const logger_1 = require("../../../logger"); const custom_1 = __importStar(require("./custom")); -const utils_1 = require("./utils"); +const thirdpartyUtils_1 = require("../../../thirdpartyUtils"); function Twitter(input) { var _a; if (input.config.name === undefined) { @@ -116,7 +116,7 @@ function Twitter(input) { }, originalImplementation.config.tokenEndpointBodyParams ); - const tokenResponse = await utils_1.doPostRequest( + const tokenResponse = await thirdpartyUtils_1.doPostRequest( originalImplementation.config.tokenEndpoint, twitterOauthTokenParams, { diff --git a/lib/build/recipe/thirdparty/providers/utils.d.ts b/lib/build/recipe/thirdparty/providers/utils.d.ts index e09a4d2bc..05e453182 100644 --- a/lib/build/recipe/thirdparty/providers/utils.d.ts +++ b/lib/build/recipe/thirdparty/providers/utils.d.ts @@ -1,36 +1,4 @@ // @ts-nocheck -import * as jose from "jose"; import { ProviderConfigForClientType } from "../types"; -export declare function doGetRequest( - url: string, - queryParams?: { - [key: string]: string; - }, - headers?: { - [key: string]: string; - } -): Promise<{ - jsonResponse: Record | undefined; - status: number; - stringResponse: string; -}>; -export declare function doPostRequest( - url: string, - params: { - [key: string]: any; - }, - headers?: { - [key: string]: string; - } -): Promise<{ - jsonResponse: Record | undefined; - status: number; - stringResponse: string; -}>; -export declare function verifyIdTokenFromJWKSEndpointAndGetPayload( - idToken: string, - jwks: jose.JWTVerifyGetKey, - otherOptions: jose.JWTVerifyOptions -): Promise; export declare function discoverOIDCEndpoints(config: ProviderConfigForClientType): Promise; export declare function normaliseOIDCEndpointToIncludeWellKnown(url: string): string; diff --git a/lib/build/recipe/thirdparty/providers/utils.js b/lib/build/recipe/thirdparty/providers/utils.js index df183ed5c..4b2590693 100644 --- a/lib/build/recipe/thirdparty/providers/utils.js +++ b/lib/build/recipe/thirdparty/providers/utils.js @@ -1,131 +1,17 @@ "use strict"; -var __createBinding = - (this && this.__createBinding) || - (Object.create - ? function (o, m, k, k2) { - if (k2 === undefined) k2 = k; - Object.defineProperty(o, k2, { - enumerable: true, - get: function () { - return m[k]; - }, - }); - } - : function (o, m, k, k2) { - if (k2 === undefined) k2 = k; - o[k2] = m[k]; - }); -var __setModuleDefault = - (this && this.__setModuleDefault) || - (Object.create - ? function (o, v) { - Object.defineProperty(o, "default", { enumerable: true, value: v }); - } - : function (o, v) { - o["default"] = v; - }); -var __importStar = - (this && this.__importStar) || - function (mod) { - if (mod && mod.__esModule) return mod; - var result = {}; - if (mod != null) - for (var k in mod) - if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); - __setModuleDefault(result, mod); - return result; - }; var __importDefault = (this && this.__importDefault) || function (mod) { return mod && mod.__esModule ? mod : { default: mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); -exports.normaliseOIDCEndpointToIncludeWellKnown = exports.discoverOIDCEndpoints = exports.verifyIdTokenFromJWKSEndpointAndGetPayload = exports.doPostRequest = exports.doGetRequest = void 0; -const jose = __importStar(require("jose")); +exports.normaliseOIDCEndpointToIncludeWellKnown = exports.discoverOIDCEndpoints = void 0; +const thirdpartyUtils_1 = require("../../../thirdpartyUtils"); const normalisedURLDomain_1 = __importDefault(require("../../../normalisedURLDomain")); const normalisedURLPath_1 = __importDefault(require("../../../normalisedURLPath")); -const logger_1 = require("../../../logger"); -const utils_1 = require("../../../utils"); -async function doGetRequest(url, queryParams, headers) { - logger_1.logDebugMessage( - `GET request to ${url}, with query params ${JSON.stringify(queryParams)} and headers ${JSON.stringify(headers)}` - ); - if ((headers === null || headers === void 0 ? void 0 : headers["Accept"]) === undefined) { - headers = Object.assign(Object.assign({}, headers), { Accept: "application/json" }); - } - const finalURL = new URL(url); - finalURL.search = new URLSearchParams(queryParams).toString(); - let response = await utils_1.doFetch(finalURL.toString(), { - headers: headers, - }); - const stringResponse = await response.text(); - let jsonResponse = undefined; - if (response.status < 400) { - jsonResponse = JSON.parse(stringResponse); - } - logger_1.logDebugMessage(`Received response with status ${response.status} and body ${stringResponse}`); - return { - stringResponse, - status: response.status, - jsonResponse, - }; -} -exports.doGetRequest = doGetRequest; -async function doPostRequest(url, params, headers) { - if (headers === undefined) { - headers = {}; - } - headers["Content-Type"] = "application/x-www-form-urlencoded"; - headers["Accept"] = "application/json"; // few providers like github don't send back json response by default - logger_1.logDebugMessage( - `POST request to ${url}, with params ${JSON.stringify(params)} and headers ${JSON.stringify(headers)}` - ); - const body = new URLSearchParams(params).toString(); - let response = await utils_1.doFetch(url, { - method: "POST", - body, - headers, - }); - const stringResponse = await response.text(); - let jsonResponse = undefined; - if (response.status < 400) { - jsonResponse = JSON.parse(stringResponse); - } - logger_1.logDebugMessage(`Received response with status ${response.status} and body ${stringResponse}`); - return { - stringResponse, - status: response.status, - jsonResponse, - }; -} -exports.doPostRequest = doPostRequest; -async function verifyIdTokenFromJWKSEndpointAndGetPayload(idToken, jwks, otherOptions) { - const { payload } = await jose.jwtVerify(idToken, jwks, otherOptions); - return payload; -} -exports.verifyIdTokenFromJWKSEndpointAndGetPayload = verifyIdTokenFromJWKSEndpointAndGetPayload; -// OIDC utils -var oidcInfoMap = {}; -async function getOIDCDiscoveryInfo(issuer) { - if (oidcInfoMap[issuer] !== undefined) { - return oidcInfoMap[issuer]; - } - const normalizedDomain = new normalisedURLDomain_1.default(issuer); - const normalizedPath = new normalisedURLPath_1.default(issuer); - let oidcInfo = await doGetRequest(normalizedDomain.getAsStringDangerous() + normalizedPath.getAsStringDangerous()); - if (oidcInfo.status > 400) { - logger_1.logDebugMessage( - `Received response with status ${oidcInfo.status} and body ${oidcInfo.stringResponse}` - ); - throw new Error(`Received response with status ${oidcInfo.status} and body ${oidcInfo.stringResponse}`); - } - oidcInfoMap[issuer] = oidcInfo.jsonResponse; - return oidcInfo.jsonResponse; -} async function discoverOIDCEndpoints(config) { if (config.oidcDiscoveryEndpoint !== undefined) { - const oidcInfo = await getOIDCDiscoveryInfo(config.oidcDiscoveryEndpoint); + const oidcInfo = await thirdpartyUtils_1.getOIDCDiscoveryInfo(config.oidcDiscoveryEndpoint); if (oidcInfo.authorization_endpoint !== undefined && config.authorizationEndpoint === undefined) { config.authorizationEndpoint = oidcInfo.authorization_endpoint; } diff --git a/lib/build/recipe/thirdparty/recipeImplementation.js b/lib/build/recipe/thirdparty/recipeImplementation.js index 0e8ee80f1..dce4ef1e6 100644 --- a/lib/build/recipe/thirdparty/recipeImplementation.js +++ b/lib/build/recipe/thirdparty/recipeImplementation.js @@ -23,6 +23,7 @@ function getRecipeImplementation(querier, providers) { isVerified, tenantId, session, + shouldTryLinkingWithSessionUser, userContext, }) { const accountLinking = recipe_1.default.getInstance(); @@ -73,9 +74,10 @@ function getRecipeImplementation(querier, providers) { // 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); - const linkResult = await authUtils_1.AuthUtils.linkToSessionIfProvidedElseCreatePrimaryUserIdOrLinkByAccountInfo( + const linkResult = await authUtils_1.AuthUtils.linkToSessionIfRequiredElseCreatePrimaryUserIdOrLinkByAccountInfo( { tenantId, + shouldTryLinkingWithSessionUser, inputUser: response.user, recipeUserId: response.recipeUserId, session, @@ -101,6 +103,7 @@ function getRecipeImplementation(querier, providers) { userContext, oAuthTokens, session, + shouldTryLinkingWithSessionUser, rawUserInfoFromProvider, }) { let response = await this.manuallyCreateOrUpdateUser({ @@ -110,6 +113,7 @@ function getRecipeImplementation(querier, providers) { tenantId, isVerified, session, + shouldTryLinkingWithSessionUser, 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 041656d62..8d0198e3b 100644 --- a/lib/build/recipe/thirdparty/types.d.ts +++ b/lib/build/recipe/thirdparty/types.d.ts @@ -175,6 +175,7 @@ export declare type RecipeInterface = { }; }; session: SessionContainerInterface | undefined; + shouldTryLinkingWithSessionUser: boolean | undefined; tenantId: string; userContext: UserContext; }): Promise< @@ -214,6 +215,7 @@ export declare type RecipeInterface = { email: string; isVerified: boolean; session: SessionContainerInterface | undefined; + shouldTryLinkingWithSessionUser: boolean | undefined; tenantId: string; userContext: UserContext; }): Promise< @@ -275,6 +277,7 @@ export declare type APIInterface = { provider: TypeProvider; tenantId: string; session: SessionContainerInterface | undefined; + shouldTryLinkingWithSessionUser: boolean | undefined; options: APIOptions; userContext: UserContext; } & ( diff --git a/lib/build/recipe/totp/recipeImplementation.js b/lib/build/recipe/totp/recipeImplementation.js index 5465f884c..5ad44874b 100644 --- a/lib/build/recipe/totp/recipeImplementation.js +++ b/lib/build/recipe/totp/recipeImplementation.js @@ -100,6 +100,7 @@ function getRecipeInterface(querier, config) { existingDeviceName: input.existingDeviceName, newDeviceName: input.newDeviceName, }, + {}, input.userContext ); }, diff --git a/lib/build/recipe/usermetadata/recipeImplementation.js b/lib/build/recipe/usermetadata/recipeImplementation.js index e3aa25a5f..2fd78898b 100644 --- a/lib/build/recipe/usermetadata/recipeImplementation.js +++ b/lib/build/recipe/usermetadata/recipeImplementation.js @@ -36,6 +36,7 @@ function getRecipeInterface(querier) { userId, metadataUpdate, }, + {}, userContext ); }, diff --git a/lib/build/recipe/userroles/recipe.js b/lib/build/recipe/userroles/recipe.js index 53f5d56b6..c341f1c19 100644 --- a/lib/build/recipe/userroles/recipe.js +++ b/lib/build/recipe/userroles/recipe.js @@ -27,8 +27,10 @@ const utils_1 = require("./utils"); const supertokens_js_override_1 = __importDefault(require("supertokens-js-override")); const postSuperTokensInitCallbacks_1 = require("../../postSuperTokensInitCallbacks"); const recipe_1 = __importDefault(require("../session/recipe")); +const recipe_2 = __importDefault(require("../oauth2provider/recipe")); const userRoleClaim_1 = require("./userRoleClaim"); const permissionClaim_1 = require("./permissionClaim"); +const session_1 = require("../session"); class Recipe extends recipeModule_1.default { constructor(recipeId, appInfo, isInServerlessEnv, config) { super(recipeId, appInfo); @@ -51,6 +53,81 @@ class Recipe extends recipeModule_1.default { if (!this.config.skipAddingPermissionsToAccessToken) { recipe_1.default.getInstanceOrThrowError().addClaimFromOtherRecipe(permissionClaim_1.PermissionClaim); } + const tokenPayloadBuilder = async (user, scopes, sessionHandle, userContext) => { + let payload = {}; + const sessionInfo = await session_1.getSessionInformation(sessionHandle, userContext); + let userRoles = []; + if (scopes.includes("roles") || scopes.includes("permissions")) { + const res = await this.recipeInterfaceImpl.getRolesForUser({ + userId: user.id, + tenantId: sessionInfo.tenantId, + userContext, + }); + if (res.status !== "OK") { + throw new Error("Failed to fetch roles for the user"); + } + userRoles = res.roles; + } + if (scopes.includes("roles")) { + payload.roles = userRoles; + } + if (scopes.includes("permissions")) { + const userPermissions = new Set(); + for (const role of userRoles) { + const rolePermissions = await this.recipeInterfaceImpl.getPermissionsForRole({ + role, + userContext, + }); + if (rolePermissions.status !== "OK") { + throw new Error("Failed to fetch permissions for the role"); + } + for (const perm of rolePermissions.permissions) { + userPermissions.add(perm); + } + } + payload.permissions = Array.from(userPermissions); + } + return payload; + }; + recipe_2.default.getInstanceOrThrowError().addAccessTokenBuilderFromOtherRecipe(tokenPayloadBuilder); + recipe_2.default.getInstanceOrThrowError().addIdTokenBuilderFromOtherRecipe(tokenPayloadBuilder); + recipe_2.default + .getInstanceOrThrowError() + .addUserInfoBuilderFromOtherRecipe(async (user, _accessTokenPayload, scopes, tenantId, userContext) => { + let userInfo = {}; + let userRoles = []; + if (scopes.includes("roles") || scopes.includes("permissions")) { + const res = await this.recipeInterfaceImpl.getRolesForUser({ + userId: user.id, + tenantId, + userContext, + }); + if (res.status !== "OK") { + throw new Error("Failed to fetch roles for the user"); + } + userRoles = res.roles; + } + if (scopes.includes("roles")) { + userInfo.roles = userRoles; + } + if (scopes.includes("permissions")) { + const userPermissions = new Set(); + for (const role of userRoles) { + const rolePermissions = await this.recipeInterfaceImpl.getPermissionsForRole({ + role, + userContext, + }); + if (rolePermissions.status !== "OK") { + throw new Error("Failed to fetch permissions for the role"); + } + for (const perm of rolePermissions.permissions) { + userPermissions.add(perm); + } + } + userInfo.permissions = Array.from(userPermissions); + } + return userInfo; + }); }); } /* Init functions */ diff --git a/lib/build/recipe/userroles/recipeImplementation.js b/lib/build/recipe/userroles/recipeImplementation.js index ca12893f9..47dbdc1ba 100644 --- a/lib/build/recipe/userroles/recipeImplementation.js +++ b/lib/build/recipe/userroles/recipeImplementation.js @@ -29,6 +29,7 @@ function getRecipeInterface(querier) { `/${tenantId === undefined ? constants_1.DEFAULT_TENANT_ID : tenantId}/recipe/user/role` ), { userId, role }, + {}, userContext ); }, @@ -63,6 +64,7 @@ function getRecipeInterface(querier) { return querier.sendPutRequest( new normalisedURLPath_1.default("/recipe/role"), { role, permissions }, + {}, userContext ); }, diff --git a/lib/build/recipeModule.d.ts b/lib/build/recipeModule.d.ts index 778cd14ee..e277192a9 100644 --- a/lib/build/recipeModule.d.ts +++ b/lib/build/recipeModule.d.ts @@ -5,7 +5,7 @@ import NormalisedURLPath from "./normalisedURLPath"; import { BaseRequest, BaseResponse } from "./framework"; export default abstract class RecipeModule { private recipeId; - private appInfo; + protected appInfo: NormalisedAppinfo; constructor(recipeId: string, appInfo: NormalisedAppinfo); getRecipeId: () => string; getAppInfo: () => NormalisedAppinfo; @@ -17,6 +17,7 @@ export default abstract class RecipeModule { | { id: string; tenantId: string; + exactMatch: boolean; } | undefined >; diff --git a/lib/build/recipeModule.js b/lib/build/recipeModule.js index 7b3eef889..75c3219ae 100644 --- a/lib/build/recipeModule.js +++ b/lib/build/recipeModule.js @@ -54,7 +54,7 @@ class RecipeModule { tenantIdFromFrontend: constants_1.DEFAULT_TENANT_ID, userContext, }); - return { id: currAPI.id, tenantId: finalTenantId }; + return { id: currAPI.id, tenantId: finalTenantId, exactMatch: true }; } else if ( remainingPath !== undefined && this.appInfo.apiBasePath @@ -65,7 +65,7 @@ class RecipeModule { tenantIdFromFrontend: tenantId === undefined ? constants_1.DEFAULT_TENANT_ID : tenantId, userContext, }); - return { id: currAPI.id, tenantId: finalTenantId }; + return { id: currAPI.id, tenantId: finalTenantId, exactMatch: false }; } } } diff --git a/lib/build/supertokens.js b/lib/build/supertokens.js index 7ee945c3b..2da1ac7bb 100644 --- a/lib/build/supertokens.js +++ b/lib/build/supertokens.js @@ -135,6 +135,7 @@ class SuperTokens { userIdType: input.userIdType, externalUserIdInfo: input.externalUserIdInfo, }, + {}, input.userContext ); } else { @@ -162,6 +163,7 @@ class SuperTokens { requestRID = undefined; } async function handleWithoutRid(recipeModules) { + let bestMatch = undefined; for (let i = 0; i < recipeModules.length; i++) { logger_1.logDebugMessage( "middleware: Checking recipe ID for match: " + @@ -173,26 +175,40 @@ class SuperTokens { ); let idResult = await recipeModules[i].returnAPIIdIfCanHandleRequest(path, method, userContext); if (idResult !== undefined) { - logger_1.logDebugMessage("middleware: Request being handled by recipe. ID is: " + idResult.id); - let requestHandled = await recipeModules[i].handleAPIRequest( - idResult.id, - idResult.tenantId, - request, - response, - path, - method, - userContext - ); - if (!requestHandled) { - logger_1.logDebugMessage( - "middleware: Not handled because API returned requestHandled as false" - ); - return false; + // The request path may or may not include the tenantId. `returnAPIIdIfCanHandleRequest` handles both cases. + // If one recipe matches with tenantId and another matches exactly, we prefer the exact match. + if (bestMatch === undefined || idResult.exactMatch) { + bestMatch = { + recipeModule: recipeModules[i], + idResult: idResult, + }; + } + if (idResult.exactMatch) { + break; } - logger_1.logDebugMessage("middleware: Ended"); - return true; } } + if (bestMatch !== undefined) { + const { idResult, recipeModule } = bestMatch; + logger_1.logDebugMessage("middleware: Request being handled by recipe. ID is: " + idResult.id); + let requestHandled = await recipeModule.handleAPIRequest( + idResult.id, + idResult.tenantId, + request, + response, + path, + method, + userContext + ); + if (!requestHandled) { + logger_1.logDebugMessage( + "middleware: Not handled because API returned requestHandled as false" + ); + return false; + } + logger_1.logDebugMessage("middleware: Ended"); + return true; + } logger_1.logDebugMessage("middleware: Not handling because no recipe matched"); return false; } @@ -238,13 +254,18 @@ class SuperTokens { // the path and methods of the APIs exposed via those recipes is unique. let currIdResult = await matchedRecipe[i].returnAPIIdIfCanHandleRequest(path, method, userContext); if (currIdResult !== undefined) { - if (idResult !== undefined) { + if ( + idResult === undefined || + // The request path may or may not include the tenantId. `returnAPIIdIfCanHandleRequest` handles both cases. + // If one recipe matches with tenantId and another matches exactly, we prefer the exact match. + (currIdResult.exactMatch === true && idResult.exactMatch === false) + ) { + finalMatchedRecipe = matchedRecipe[i]; + idResult = currIdResult; + } else { throw new Error( "Two recipes have matched the same API path and method! This is a bug in the SDK. Please contact support." ); - } else { - finalMatchedRecipe = matchedRecipe[i]; - idResult = currIdResult; } } } @@ -355,6 +376,7 @@ class SuperTokens { let totpFound = false; let userMetadataFound = false; let multiFactorAuthFound = false; + let oauth2Found = false; // Multitenancy recipe is an always initialized recipe and needs to be imported this way // so that there is no circular dependency. Otherwise there would be cyclic dependency // between `supertokens.ts` -> `recipeModule.ts` -> `multitenancy/recipe.ts` @@ -362,6 +384,7 @@ class SuperTokens { let UserMetadataRecipe = require("./recipe/usermetadata/recipe").default; let MultiFactorAuthRecipe = require("./recipe/multifactorauth/recipe").default; let TotpRecipe = require("./recipe/totp/recipe").default; + let OAuth2ProviderRecipe = require("./recipe/oauth2provider/recipe").default; this.recipeModules = config.recipeList.map((func) => { const recipeModule = func(this.appInfo, this.isInServerlessEnv); if (recipeModule.getRecipeId() === MultitenancyRecipe.RECIPE_ID) { @@ -372,6 +395,8 @@ class SuperTokens { multiFactorAuthFound = true; } else if (recipeModule.getRecipeId() === TotpRecipe.RECIPE_ID) { totpFound = true; + } else if (recipeModule.getRecipeId() === OAuth2ProviderRecipe.RECIPE_ID) { + oauth2Found = true; } return recipeModule; }); @@ -390,6 +415,10 @@ class SuperTokens { // the app doesn't have to do that if they only use TOTP (which shouldn't be that uncommon) // To let those cases function without initializing account linking we do not check it here, but when // the authentication endpoints are called. + // We've decided to always initialize the OAuth2Provider recipe + if (!oauth2Found) { + this.recipeModules.push(OAuth2ProviderRecipe.init()(this.appInfo, this.isInServerlessEnv)); + } this.telemetryEnabled = config.telemetry === undefined ? process.env.TEST_MODE !== "testing" : config.telemetry; } static init(config) { diff --git a/lib/build/thirdpartyUtils.d.ts b/lib/build/thirdpartyUtils.d.ts new file mode 100644 index 000000000..84517d5e2 --- /dev/null +++ b/lib/build/thirdpartyUtils.d.ts @@ -0,0 +1,34 @@ +// @ts-nocheck +import * as jose from "jose"; +export declare function doGetRequest( + url: string, + queryParams?: { + [key: string]: string; + }, + headers?: { + [key: string]: string; + } +): Promise<{ + jsonResponse: Record | undefined; + status: number; + stringResponse: string; +}>; +export declare function doPostRequest( + url: string, + params: { + [key: string]: any; + }, + headers?: { + [key: string]: string; + } +): Promise<{ + jsonResponse: Record | undefined; + status: number; + stringResponse: string; +}>; +export declare function verifyIdTokenFromJWKSEndpointAndGetPayload( + idToken: string, + jwks: jose.JWTVerifyGetKey, + otherOptions: jose.JWTVerifyOptions +): Promise; +export declare function getOIDCDiscoveryInfo(issuer: string): Promise; diff --git a/lib/build/thirdpartyUtils.js b/lib/build/thirdpartyUtils.js new file mode 100644 index 000000000..83cc3d405 --- /dev/null +++ b/lib/build/thirdpartyUtils.js @@ -0,0 +1,128 @@ +"use strict"; +var __createBinding = + (this && this.__createBinding) || + (Object.create + ? function (o, m, k, k2) { + if (k2 === undefined) k2 = k; + Object.defineProperty(o, k2, { + enumerable: true, + get: function () { + return m[k]; + }, + }); + } + : function (o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; + }); +var __setModuleDefault = + (this && this.__setModuleDefault) || + (Object.create + ? function (o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); + } + : function (o, v) { + o["default"] = v; + }); +var __importStar = + (this && this.__importStar) || + function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) + for (var k in mod) + if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); + __setModuleDefault(result, mod); + return result; + }; +var __importDefault = + (this && this.__importDefault) || + function (mod) { + return mod && mod.__esModule ? mod : { default: mod }; + }; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.getOIDCDiscoveryInfo = exports.verifyIdTokenFromJWKSEndpointAndGetPayload = exports.doPostRequest = exports.doGetRequest = void 0; +const jose = __importStar(require("jose")); +const logger_1 = require("./logger"); +const utils_1 = require("./utils"); +const normalisedURLDomain_1 = __importDefault(require("./normalisedURLDomain")); +const normalisedURLPath_1 = __importDefault(require("./normalisedURLPath")); +async function doGetRequest(url, queryParams, headers) { + logger_1.logDebugMessage( + `GET request to ${url}, with query params ${JSON.stringify(queryParams)} and headers ${JSON.stringify(headers)}` + ); + if ((headers === null || headers === void 0 ? void 0 : headers["Accept"]) === undefined) { + headers = Object.assign(Object.assign({}, headers), { Accept: "application/json" }); + } + const finalURL = new URL(url); + finalURL.search = new URLSearchParams(queryParams).toString(); + let response = await utils_1.doFetch(finalURL.toString(), { + headers: headers, + }); + const stringResponse = await response.text(); + let jsonResponse = undefined; + if (response.status < 400) { + jsonResponse = JSON.parse(stringResponse); + } + logger_1.logDebugMessage(`Received response with status ${response.status} and body ${stringResponse}`); + return { + stringResponse, + status: response.status, + jsonResponse, + }; +} +exports.doGetRequest = doGetRequest; +async function doPostRequest(url, params, headers) { + if (headers === undefined) { + headers = {}; + } + headers["Content-Type"] = "application/x-www-form-urlencoded"; + headers["Accept"] = "application/json"; + logger_1.logDebugMessage( + `POST request to ${url}, with params ${JSON.stringify(params)} and headers ${JSON.stringify(headers)}` + ); + const body = new URLSearchParams(params).toString(); + let response = await utils_1.doFetch(url, { + method: "POST", + body, + headers, + }); + const stringResponse = await response.text(); + let jsonResponse = undefined; + if (response.status < 400) { + jsonResponse = JSON.parse(stringResponse); + } + logger_1.logDebugMessage(`Received response with status ${response.status} and body ${stringResponse}`); + return { + stringResponse, + status: response.status, + jsonResponse, + }; +} +exports.doPostRequest = doPostRequest; +async function verifyIdTokenFromJWKSEndpointAndGetPayload(idToken, jwks, otherOptions) { + const { payload } = await jose.jwtVerify(idToken, jwks, otherOptions); + return payload; +} +exports.verifyIdTokenFromJWKSEndpointAndGetPayload = verifyIdTokenFromJWKSEndpointAndGetPayload; +// OIDC utils +var oidcInfoMap = {}; +async function getOIDCDiscoveryInfo(issuer) { + const normalizedDomain = new normalisedURLDomain_1.default(issuer); + let normalizedPath = new normalisedURLPath_1.default(issuer); + if (oidcInfoMap[issuer] !== undefined) { + return oidcInfoMap[issuer]; + } + const oidcInfo = await doGetRequest( + normalizedDomain.getAsStringDangerous() + normalizedPath.getAsStringDangerous() + ); + if (oidcInfo.status >= 400) { + logger_1.logDebugMessage( + `Received response with status ${oidcInfo.status} and body ${oidcInfo.stringResponse}` + ); + throw new Error(`Received response with status ${oidcInfo.status} and body ${oidcInfo.stringResponse}`); + } + oidcInfoMap[issuer] = oidcInfo.jsonResponse; + return oidcInfo.jsonResponse; +} +exports.getOIDCDiscoveryInfo = getOIDCDiscoveryInfo; diff --git a/lib/build/types.d.ts b/lib/build/types.d.ts index 4ee670175..c45e6de40 100644 --- a/lib/build/types.d.ts +++ b/lib/build/types.d.ts @@ -10,6 +10,9 @@ declare type Brand = { [__brand]: B; }; declare type Branded = T & Brand; +export declare type NonNullableProperties = { + [P in keyof T]: NonNullable; +}; export declare type UserContext = Branded, "UserContext">; export declare type AppInfo = { appName: string; @@ -62,7 +65,7 @@ export declare type APIHandled = { id: string; disabled: boolean; }; -export declare type HTTPMethod = "post" | "get" | "delete" | "put" | "options" | "trace"; +export declare type HTTPMethod = "post" | "get" | "delete" | "put" | "patch" | "options" | "trace"; export declare type JSONPrimitive = string | number | boolean | null; export declare type JSONArray = Array; export declare type JSONValue = JSONPrimitive | JSONObject | JSONArray | undefined; diff --git a/lib/build/utils.d.ts b/lib/build/utils.d.ts index 0659b1aae..8743e5767 100644 --- a/lib/build/utils.d.ts +++ b/lib/build/utils.d.ts @@ -12,6 +12,7 @@ export declare function sendNon200ResponseWithMessage(res: BaseResponse, message export declare function sendNon200Response(res: BaseResponse, statusCode: number, body: JSONObject): void; export declare function send200Response(res: BaseResponse, responseJson: any): void; export declare function isAnIpAddress(ipaddress: string): boolean; +export declare function getNormalisedShouldTryLinkingWithSessionUserFlag(req: BaseRequest, body: any): any; export declare function getBackwardsCompatibleUserInfo( req: BaseRequest, result: { @@ -57,3 +58,11 @@ export declare function postWithFetch( } >; export declare function normaliseEmail(email: string): string; +export declare function toCamelCase(str: string): string; +export declare function toSnakeCase(str: string): string; +export declare function transformObjectKeys( + obj: { + [key: string]: any; + }, + caseType: "snake-case" | "camelCase" +): T; diff --git a/lib/build/utils.js b/lib/build/utils.js index 741fda01f..30bbed8ea 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.getUserContext = exports.makeDefaultUserContextFromAPI = exports.humaniseMilliseconds = exports.frontendHasInterceptor = exports.getRidFromHeader = exports.hasGreaterThanEqualToFDI = exports.getLatestFDIVersionFromFDIList = exports.getBackwardsCompatibleUserInfo = exports.isAnIpAddress = exports.send200Response = exports.sendNon200Response = exports.sendNon200ResponseWithMessage = exports.normaliseHttpMethod = exports.normaliseInputAppInfoOrThrowError = exports.maxVersion = exports.getLargestVersionFromIntersection = exports.doFetch = void 0; +exports.transformObjectKeys = exports.toSnakeCase = exports.toCamelCase = exports.normaliseEmail = exports.postWithFetch = exports.getFromObjectCaseInsensitive = exports.getTopLevelDomainForSameSiteResolution = exports.setRequestInUserContextIfNotDefined = exports.getUserContext = exports.makeDefaultUserContextFromAPI = exports.humaniseMilliseconds = exports.frontendHasInterceptor = exports.getRidFromHeader = exports.hasGreaterThanEqualToFDI = exports.getLatestFDIVersionFromFDIList = exports.getBackwardsCompatibleUserInfo = exports.getNormalisedShouldTryLinkingWithSessionUserFlag = 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")); @@ -58,6 +58,7 @@ const doFetch = async (input, init) => { ); init = { cache: "no-cache", + redirect: "manual", }; } else { if (init.cache === undefined) { @@ -65,6 +66,7 @@ const doFetch = async (input, init) => { processState_1.PROCESS_STATE.ADDING_NO_CACHE_HEADER_IN_FETCH ); init.cache = "no-cache"; + init.redirect = "manual"; } } const fetchFunction = typeof fetch !== "undefined" ? fetch : cross_fetch_1.default; @@ -222,6 +224,14 @@ function isAnIpAddress(ipaddress) { ); } exports.isAnIpAddress = isAnIpAddress; +function getNormalisedShouldTryLinkingWithSessionUserFlag(req, body) { + var _a; + if (hasGreaterThanEqualToFDI(req, "3.1")) { + return (_a = body.shouldTryLinkingWithSessionUser) !== null && _a !== void 0 ? _a : false; + } + return undefined; +} +exports.getNormalisedShouldTryLinkingWithSessionUserFlag = getNormalisedShouldTryLinkingWithSessionUserFlag; function getBackwardsCompatibleUserInfo(req, result, userContext) { let resp; // (>= 1.18 && < 2.0) || >= 3.0: This is because before 1.18, and between 2 and 3, FDI does not @@ -425,3 +435,23 @@ function normaliseEmail(email) { return email; } exports.normaliseEmail = normaliseEmail; +function toCamelCase(str) { + return str.replace(/([-_][a-z])/gi, (match) => { + return match.toUpperCase().replace("-", "").replace("_", ""); + }); +} +exports.toCamelCase = toCamelCase; +function toSnakeCase(str) { + return str.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`); +} +exports.toSnakeCase = toSnakeCase; +// Transforms the keys of an object from camelCase to snakeCase or vice versa. +function transformObjectKeys(obj, caseType) { + const transformKey = caseType === "camelCase" ? toCamelCase : toSnakeCase; + return Object.entries(obj).reduce((result, [key, value]) => { + const transformedKey = transformKey(key); + result[transformedKey] = value; + return result; + }, {}); +} +exports.transformObjectKeys = transformObjectKeys; diff --git a/lib/build/version.d.ts b/lib/build/version.d.ts index 46c0c5e1a..ba4e3898a 100644 --- a/lib/build/version.d.ts +++ b/lib/build/version.d.ts @@ -1,4 +1,4 @@ // @ts-nocheck -export declare const version = "20.0.0"; +export declare const version = "20.0.2"; export declare const cdiSupported: string[]; export declare const dashboardVersion = "0.13"; diff --git a/lib/build/version.js b/lib/build/version.js index 17148bd6b..0d9902d49 100644 --- a/lib/build/version.js +++ b/lib/build/version.js @@ -15,7 +15,7 @@ exports.dashboardVersion = exports.cdiSupported = exports.version = void 0; * License for the specific language governing permissions and limitations * under the License. */ -exports.version = "20.0.0"; +exports.version = "20.0.2"; exports.cdiSupported = ["5.1"]; // Note: The actual script import for dashboard uses v{DASHBOARD_VERSION} exports.dashboardVersion = "0.13"; diff --git a/lib/ts/authUtils.ts b/lib/ts/authUtils.ts index 0443941bf..551563934 100644 --- a/lib/ts/authUtils.ts +++ b/lib/ts/authUtils.ts @@ -10,7 +10,7 @@ import RecipeUserId from "./recipeUserId"; import { updateAndGetMFARelatedInfoInSession } from "./recipe/multifactorauth/utils"; import { isValidFirstFactor } from "./recipe/multitenancy/utils"; import SessionError from "./recipe/session/error"; -import { getUser } from "."; +import { Error as STError, getUser } from "."; import { AccountInfoWithRecipeId } from "./recipe/accountlinking/types"; import { BaseRequest, BaseResponse } from "./framework"; import SessionRecipe from "./recipe/session/recipe"; @@ -82,6 +82,7 @@ export const AuthUtils = { factorIds, skipSessionUserUpdateInCore, session, + shouldTryLinkingWithSessionUser, userContext, }: { authenticatingAccountInfo: AccountInfoWithRecipeId; @@ -93,6 +94,7 @@ export const AuthUtils = { signInVerifiesLoginMethod: boolean; skipSessionUserUpdateInCore: boolean; session?: SessionContainerInterface; + shouldTryLinkingWithSessionUser: boolean | undefined; userContext: UserContext; }): Promise< | { status: "OK"; validFactorIds: string[]; isFirstFactor: boolean } @@ -118,6 +120,7 @@ export const AuthUtils = { // We also load the session user here if it is available. const authTypeInfo = await AuthUtils.checkAuthTypeAndLinkingStatus( session, + shouldTryLinkingWithSessionUser, authenticatingAccountInfo, authenticatingUser, skipSessionUserUpdateInCore, @@ -287,6 +290,9 @@ export const AuthUtils = { } } } else { + // We do not have to care about overwriting the session here, since we either: + // - have overwriteSessionDuringSignInUp true and can ignore it + // - have overwriteSessionDuringSignInUp false and we checked in the api imlp that there is no session logDebugMessage(`postAuthChecks creating session for first factor sign in/up`); // If there is no input session, we do not need to do anything other checks and create a new session respSession = await Session.createNewSession(req, res, tenantId, recipeUserId, {}, {}, userContext); @@ -480,6 +486,7 @@ export const AuthUtils = { */ checkAuthTypeAndLinkingStatus: async function ( session: SessionContainerInterface | undefined, + shouldTryLinkingWithSessionUser: boolean | undefined, accountInfo: AccountInfoWithRecipeId, inputUser: User | undefined, skipSessionUserUpdateInCore: boolean, @@ -503,17 +510,36 @@ export const AuthUtils = { logDebugMessage(`checkAuthTypeAndLinkingStatus called`); let sessionUser: User | undefined = undefined; if (session === undefined) { + if (shouldTryLinkingWithSessionUser === true) { + throw new SessionError({ + type: SessionError.UNAUTHORISED, + message: "Session not found but shouldTryLinkingWithSessionUser is true", + }); + } logDebugMessage(`checkAuthTypeAndLinkingStatus returning first factor because there is no session`); // If there is no active session we have nothing to link to - so this has to be a first factor sign in return { status: "OK", isFirstFactor: true }; } else { + if (shouldTryLinkingWithSessionUser === false) { + // In our normal flows this should never happen - but some user overrides might do this. + // Anyway, since shouldTryLinkingWithSessionUser explicitly set to false, it's safe to consider this a firstFactor + return { status: "OK", isFirstFactor: true }; + } + if (!recipeInitDefinedShouldDoAutomaticAccountLinking(AccountLinking.getInstance().config)) { - if (MultiFactorAuthRecipe.getInstance() !== undefined) { + if (shouldTryLinkingWithSessionUser === true) { throw new Error( "Please initialise the account linking recipe and define shouldDoAutomaticAccountLinking to enable MFA" ); } else { - return { status: "OK", isFirstFactor: true }; + // This is the legacy case where shouldTryLinkingWithSessionUser is undefined + if (MultiFactorAuthRecipe.getInstance() !== undefined) { + throw new Error( + "Please initialise the account linking recipe and define shouldDoAutomaticAccountLinking to enable MFA" + ); + } else { + return { status: "OK", isFirstFactor: true }; + } } } @@ -542,6 +568,14 @@ export const AuthUtils = { userContext ); if (sessionUserResult.status === "SHOULD_AUTOMATICALLY_LINK_FALSE") { + if (shouldTryLinkingWithSessionUser === true) { + throw new STError({ + message: + "shouldDoAutomaticAccountLinking returned false when creating primary user but shouldTryLinkingWithSessionUser is true", + type: "BAD_INPUT_ERROR", + }); + } + return { status: "OK", isFirstFactor: true, @@ -572,6 +606,13 @@ export const AuthUtils = { ); if (shouldLink.shouldAutomaticallyLink === false) { + if (shouldTryLinkingWithSessionUser === true) { + throw new STError({ + message: + "shouldDoAutomaticAccountLinking returned false when creating primary user but shouldTryLinkingWithSessionUser is true", + type: "BAD_INPUT_ERROR", + }); + } return { status: "OK", isFirstFactor: true }; } else { return { @@ -599,17 +640,19 @@ export const AuthUtils = { * - LINKING_TO_SESSION_USER_FAILED (SESSION_USER_ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR): * if the session user should be primary but we couldn't make it primary because of a conflicting primary user. */ - linkToSessionIfProvidedElseCreatePrimaryUserIdOrLinkByAccountInfo: async function ({ + linkToSessionIfRequiredElseCreatePrimaryUserIdOrLinkByAccountInfo: async function ({ tenantId, inputUser, recipeUserId, session, + shouldTryLinkingWithSessionUser, userContext, }: { tenantId: string; inputUser: User; recipeUserId: RecipeUserId; session: SessionContainerInterface | undefined; + shouldTryLinkingWithSessionUser: boolean | undefined; userContext: UserContext; }): Promise< | { status: "OK"; user: User } @@ -625,10 +668,11 @@ export const AuthUtils = { logDebugMessage("linkToSessionIfProvidedElseCreatePrimaryUserIdOrLinkByAccountInfo called"); const retry = () => { logDebugMessage("linkToSessionIfProvidedElseCreatePrimaryUserIdOrLinkByAccountInfo retrying...."); - return AuthUtils.linkToSessionIfProvidedElseCreatePrimaryUserIdOrLinkByAccountInfo({ + return AuthUtils.linkToSessionIfRequiredElseCreatePrimaryUserIdOrLinkByAccountInfo({ tenantId, inputUser: inputUser, session, + shouldTryLinkingWithSessionUser, recipeUserId, userContext, }); @@ -647,6 +691,7 @@ export const AuthUtils = { const authTypeRes = await AuthUtils.checkAuthTypeAndLinkingStatus( session, + shouldTryLinkingWithSessionUser, authLoginMethod, inputUser, false, @@ -959,6 +1004,26 @@ export const AuthUtils = { return validFactorIds; }, + loadSessionInAuthAPIIfNeeded: async function ( + req: BaseRequest, + res: BaseResponse, + shouldTryLinkingWithSessionUser: boolean | undefined, + userContext: UserContext + ) { + const overwriteSessionDuringSignInUp = SessionRecipe.getInstanceOrThrowError().config + .overwriteSessionDuringSignInUp; + return shouldTryLinkingWithSessionUser !== false || !overwriteSessionDuringSignInUp + ? await Session.getSession( + req, + res, + { + sessionRequired: shouldTryLinkingWithSessionUser === true, + overrideGlobalClaimValidators: () => [], + }, + userContext + ) + : undefined; + }, }; async function filterOutInvalidSecondFactorsOrThrowIfAllAreInvalid( diff --git a/lib/ts/combinedRemoteJWKSet.ts b/lib/ts/combinedRemoteJWKSet.ts new file mode 100644 index 000000000..9da2b8b07 --- /dev/null +++ b/lib/ts/combinedRemoteJWKSet.ts @@ -0,0 +1,62 @@ +import { createRemoteJWKSet } from "jose"; +import { JWKCacheCooldownInMs } from "./recipe/session/constants"; +import { Querier } from "./querier"; + +let combinedJWKS: ReturnType | undefined; + +/** + * We need this to reset the combinedJWKS in tests because we need to create a new instance of the combinedJWKS + * for each test to avoid caching issues. + * This is called when the session recipe is reset and when the oauth2provider recipe is reset. + * Calling this multiple times doesn't cause an issue. + */ +export function resetCombinedJWKS() { + combinedJWKS = undefined; +} + +// TODO: remove this after proper core support +const hydraJWKS = createRemoteJWKSet(new URL("http://localhost:4444/.well-known/jwks.json"), { + cooldownDuration: JWKCacheCooldownInMs, +}); +/** + The function returned by this getter fetches all JWKs from the first available core instance. + This combines the other JWKS functions to become error resistant. + + Every core instance a backend is connected to is expected to connect to the same database and use the same key set for + token verification. Otherwise, the result of session verification would depend on which core is currently available. +*/ +export function getCombinedJWKS() { + if (combinedJWKS === undefined) { + const JWKS: ReturnType[] = Querier.getNewInstanceOrThrowError(undefined) + .getAllCoreUrlsForPath("/.well-known/jwks.json") + .map((url) => + createRemoteJWKSet(new URL(url), { + cooldownDuration: JWKCacheCooldownInMs, + }) + ); + + combinedJWKS = async (...args) => { + let lastError = undefined; + + if (!args[0]?.kid?.startsWith("s-") && !args[0]?.kid?.startsWith("d-")) { + return hydraJWKS(...args); + } + + if (JWKS.length === 0) { + throw Error( + "No SuperTokens core available to query. Please pass supertokens > connectionURI to the init function, or override all the functions of the recipe you are using." + ); + } + for (const jwks of JWKS) { + try { + // We await before returning to make sure we catch the error + return await jwks(...args); + } catch (ex) { + lastError = ex; + } + } + throw lastError; + }; + } + return combinedJWKS; +} diff --git a/lib/ts/framework/request.ts b/lib/ts/framework/request.ts index 7e24b9f3b..5bac2548f 100644 --- a/lib/ts/framework/request.ts +++ b/lib/ts/framework/request.ts @@ -63,4 +63,26 @@ export abstract class BaseRequest { } return this.parsedJSONBody; }; + + getBodyAsJSONOrFormData = async (): Promise => { + const contentType = this.getHeaderValue("content-type"); + + if (contentType) { + if (contentType.startsWith("application/json")) { + return await this.getJSONBody(); + } else if (contentType.startsWith("application/x-www-form-urlencoded")) { + return await this.getFormData(); + } + } else { + try { + return await this.getJSONBody(); + } catch { + try { + return await this.getFormData(); + } catch { + throw new Error("Unable to parse body as JSON or Form Data."); + } + } + } + }; } diff --git a/lib/ts/querier.ts b/lib/ts/querier.ts index 8cedfdbcb..895239ca9 100644 --- a/lib/ts/querier.ts +++ b/lib/ts/querier.ts @@ -23,6 +23,11 @@ import { UserContext } from "./types"; import { NetworkInterceptor } from "./types"; import SuperTokens from "./supertokens"; +export const hydraPubDomain = process.env.HYDRA_PUB ?? "http://localhost:4444"; // This will be used as a domain for paths starting with hydraPubPathPrefix +const hydraAdmDomain = process.env.HYDRA_ADM ?? "http://localhost:4445"; // This will be used as a domain for paths starting with hydraAdmPathPrefix +const hydraPubPathPrefix = "/recipe/oauth2/pub"; // Replaced with "/oauth2" when sending the request (/recipe/oauth2/pub/token -> /oauth2/token) +const hydraAdmPathPrefix = "/recipe/oauth2/admin"; // Replaced with "/admin" when sending the request (/recipe/oauth2/admin/clients -> /admin/clients) + export class Querier { private static initCalled = false; private static hosts: { domain: NormalisedURLDomain; basePath: NormalisedURLPath }[] | undefined = undefined; @@ -155,6 +160,11 @@ export class Querier { // path should start with "/" sendPostRequest = async (path: NormalisedURLPath, body: any, userContext: UserContext): Promise => { this.invalidateCoreCallCache(userContext); + // TODO: remove FormData + const isForm = body !== undefined && body["$isFormData"]; + if (isForm) { + delete body["$isFormData"]; + } const { body: respBody } = await this.sendRequestHelper( path, @@ -163,8 +173,19 @@ export class Querier { let apiVersion = await this.getAPIVersion(userContext); let headers: any = { "cdi-version": apiVersion, - "content-type": "application/json; charset=utf-8", }; + if (isForm) { + headers["content-type"] = "application/x-www-form-urlencoded"; + } else { + headers["content-type"] = "application/json; charset=utf-8"; + } + + // TODO: Remove this after core changes are done + if (body !== undefined && body["authorizationHeader"]) { + headers["authorization"] = body["authorizationHeader"]; + delete body["authorizationHeader"]; + } + if (Querier.apiKey !== undefined) { headers = { ...headers, @@ -195,7 +216,11 @@ export class Querier { } return doFetch(url, { method: "POST", - body: body !== undefined ? JSON.stringify(body) : undefined, + body: isForm + ? new URLSearchParams(Object.entries(body)).toString() + : body !== undefined + ? JSON.stringify(body) + : undefined, headers, }); }, @@ -344,12 +369,15 @@ export class Querier { finalURL.search = searchParams.toString(); // Update cache and return - let response = await doFetch(finalURL.toString(), { method: "GET", headers, }); + if (response.status === 302) { + return response; + } + if (response.status === 200 && !Querier.disableCache) { // If the request was successful, we save the result into the cache // plus we update the cache tag @@ -374,6 +402,7 @@ export class Querier { sendGetRequestWithResponseHeaders = async ( path: NormalisedURLPath, params: Record, + inpHeaders: Record | undefined, userContext: UserContext ): Promise<{ body: any; headers: Headers }> => { return await this.sendRequestHelper( @@ -381,7 +410,9 @@ export class Querier { "GET", async (url: string) => { let apiVersion = await this.getAPIVersion(userContext); - let headers: any = { "cdi-version": apiVersion }; + let headers: any = inpHeaders ?? {}; + headers["cdi-version"] = apiVersion; + if (Querier.apiKey !== undefined) { headers = { ...headers, @@ -425,7 +456,12 @@ export class Querier { }; // path should start with "/" - sendPutRequest = async (path: NormalisedURLPath, body: any, userContext: UserContext): Promise => { + sendPutRequest = async ( + path: NormalisedURLPath, + body: any, + params: Record, + userContext: UserContext + ): Promise => { this.invalidateCoreCallCache(userContext); const { body: respBody } = await this.sendRequestHelper( @@ -453,6 +489,7 @@ export class Querier { method: "put", headers: headers, body: body, + params: params, }, userContext ); @@ -463,7 +500,13 @@ export class Querier { } } - return doFetch(url, { + const finalURL = new URL(url); + const searchParams = new URLSearchParams( + Object.entries(params).filter(([_, value]) => value !== undefined) as string[][] + ); + finalURL.search = searchParams.toString(); + + return doFetch(finalURL.toString(), { method: "PUT", body: body !== undefined ? JSON.stringify(body) : undefined, headers, @@ -474,6 +517,56 @@ export class Querier { return respBody; }; + // path should start with "/" + sendPatchRequest = async (path: NormalisedURLPath, body: any, userContext: UserContext): Promise => { + this.invalidateCoreCallCache(userContext); + + const { body: respBody } = await this.sendRequestHelper( + path, + "PATCH", + async (url: string) => { + let apiVersion = await this.getAPIVersion(userContext); + let headers: any = { "cdi-version": apiVersion, "content-type": "application/json; charset=utf-8" }; + if (Querier.apiKey !== undefined) { + headers = { + ...headers, + "api-key": Querier.apiKey, + }; + } + if (path.isARecipePath() && this.rIdToCore !== undefined) { + headers = { + ...headers, + rid: this.rIdToCore, + }; + } + if (Querier.networkInterceptor !== undefined) { + let request = Querier.networkInterceptor( + { + url: url, + method: "patch", + headers: headers, + body: body, + }, + userContext + ); + url = request.url; + headers = request.headers; + if (request.body !== undefined) { + body = request.body; + } + } + + return doFetch(url, { + method: "PATCH", + body: body !== undefined ? JSON.stringify(body) : undefined, + headers, + }); + }, + this.__hosts?.length || 0 + ); + return respBody; + }; + invalidateCoreCallCache = (userContext: UserContext, updGlobalCacheTagIfNecessary = true) => { if (updGlobalCacheTagIfNecessary && userContext._default?.keepCacheAlive !== true) { Querier.globalCacheTag = Date.now(); @@ -518,7 +611,23 @@ export class Querier { } let currentDomain: string = this.__hosts[Querier.lastTriedIndex].domain.getAsStringDangerous(); let currentBasePath: string = this.__hosts[Querier.lastTriedIndex].basePath.getAsStringDangerous(); - const url = currentDomain + currentBasePath + path.getAsStringDangerous(); + + let strPath = path.getAsStringDangerous(); + const isHydraAPICall = strPath.startsWith(hydraAdmPathPrefix) || strPath.startsWith(hydraPubPathPrefix); + + if (strPath.startsWith(hydraPubPathPrefix)) { + currentDomain = hydraPubDomain; + currentBasePath = ""; + strPath = strPath.replace(hydraPubPathPrefix, "/oauth2"); + } + + if (strPath.startsWith(hydraAdmPathPrefix)) { + currentDomain = hydraAdmDomain; + currentBasePath = ""; + strPath = strPath.replace(hydraAdmPathPrefix, "/admin"); + } + + const url = currentDomain + currentBasePath + strPath; const maxRetries = 5; if (retryInfoMap === undefined) { @@ -538,6 +647,12 @@ export class Querier { if (process.env.TEST_MODE === "testing") { Querier.hostsAliveForTesting.add(currentDomain + currentBasePath); } + + // TODO: Temporary solution for handling Hydra API calls. Remove when Hydra is no longer called directly. + if (isHydraAPICall) { + return handleHydraAPICall(response); + } + if (response.status !== 200) { throw response; } @@ -588,3 +703,29 @@ export class Querier { } }; } + +async function handleHydraAPICall(response: Response) { + const contentType = response.headers.get("Content-Type"); + + if (contentType?.startsWith("application/json")) { + return { + body: { + status: response.ok ? "OK" : "ERROR", + statusCode: response.status, + data: await response.clone().json(), + }, + headers: response.headers, + }; + } else if (contentType?.startsWith("text/plain")) { + return { + body: { + status: response.ok ? "OK" : "ERROR", + statusCode: response.status, + data: await response.clone().text(), + }, + headers: response.headers, + }; + } + + return { body: { status: response.ok ? "OK" : "ERROR", statusCode: response.status }, headers: response.headers }; +} diff --git a/lib/ts/recipe/accountlinking/index.ts b/lib/ts/recipe/accountlinking/index.ts index 995a341e0..e878130e2 100644 --- a/lib/ts/recipe/accountlinking/index.ts +++ b/lib/ts/recipe/accountlinking/index.ts @@ -165,6 +165,10 @@ export default class Wrapper { ) { const user = await getUser(recipeUserId.getAsString(), userContext); + if (user === undefined) { + throw new Error("Passed in recipe user id does not exist"); + } + const res = await Recipe.getInstance().isEmailChangeAllowed({ user, newEmail, diff --git a/lib/ts/recipe/accountlinking/recipe.ts b/lib/ts/recipe/accountlinking/recipe.ts index d7f5a33ce..b57b9329a 100644 --- a/lib/ts/recipe/accountlinking/recipe.ts +++ b/lib/ts/recipe/accountlinking/recipe.ts @@ -523,7 +523,7 @@ export default class Recipe extends RecipeModule { }; isEmailChangeAllowed = async (input: { - user?: User; + user: User; newEmail: string; isVerified: boolean; session: SessionContainerInterface | undefined; @@ -546,10 +546,6 @@ export default class Recipe extends RecipeModule { let inputUser = input.user; - if (inputUser === undefined) { - throw new Error("Passed in recipe user id does not exist"); - } - for (const tenantId of inputUser.tenantIds) { let existingUsersWithNewEmail = await this.recipeInterfaceImpl.listUsersByAccountInfo({ tenantId, @@ -929,7 +925,7 @@ export default class Recipe extends RecipeModule { // we can use the 0 index cause targetUser is not a primary user. let shouldDoAccountLinking = await this.config.shouldDoAutomaticAccountLinking( inputUser.loginMethods[0], - primaryUserThatCanBeLinkedToTheInputUser, + createPrimaryUserResult.user, session, tenantId, userContext diff --git a/lib/ts/recipe/dashboard/api/multitenancy/createOrUpdateThirdPartyConfig.ts b/lib/ts/recipe/dashboard/api/multitenancy/createOrUpdateThirdPartyConfig.ts index 18c936daf..83e72e604 100644 --- a/lib/ts/recipe/dashboard/api/multitenancy/createOrUpdateThirdPartyConfig.ts +++ b/lib/ts/recipe/dashboard/api/multitenancy/createOrUpdateThirdPartyConfig.ts @@ -18,7 +18,7 @@ import MultitenancyRecipe from "../../../multitenancy/recipe"; import { UserContext } from "../../../../types"; import NormalisedURLDomain from "../../../../normalisedURLDomain"; import NormalisedURLPath from "../../../../normalisedURLPath"; -import { doPostRequest } from "../../../thirdparty/providers/utils"; +import { doPostRequest } from "../../../../thirdpartyUtils"; export type Response = | { diff --git a/lib/ts/recipe/dashboard/api/multitenancy/getThirdPartyConfig.ts b/lib/ts/recipe/dashboard/api/multitenancy/getThirdPartyConfig.ts index 4c0870ac3..96585bfc4 100644 --- a/lib/ts/recipe/dashboard/api/multitenancy/getThirdPartyConfig.ts +++ b/lib/ts/recipe/dashboard/api/multitenancy/getThirdPartyConfig.ts @@ -23,7 +23,7 @@ import { ProviderConfig } from "../../../thirdparty/types"; import { UserContext } from "../../../../types"; import NormalisedURLDomain from "../../../../normalisedURLDomain"; import NormalisedURLPath from "../../../../normalisedURLPath"; -import { doGetRequest } from "../../../thirdparty/providers/utils"; +import { doGetRequest } from "../../../../thirdpartyUtils"; export type Response = | { diff --git a/lib/ts/recipe/emailpassword/api/implementation.ts b/lib/ts/recipe/emailpassword/api/implementation.ts index 843e270cd..2e35c5edf 100644 --- a/lib/ts/recipe/emailpassword/api/implementation.ts +++ b/lib/ts/recipe/emailpassword/api/implementation.ts @@ -1,6 +1,5 @@ import { APIInterface, APIOptions } from "../"; import { logDebugMessage } from "../../../logger"; -import { SessionContainerInterface } from "../../session/types"; import { GeneralErrorResponse, User, UserContext } from "../../../types"; import { getUser } from "../../../"; import AccountLinking from "../../accountlinking/recipe"; @@ -591,32 +590,10 @@ export default function getAPIImplementation(): APIInterface { formFields, tenantId, session, + shouldTryLinkingWithSessionUser, options, userContext, - }: { - formFields: { - id: string; - value: string; - }[]; - tenantId: string; - session?: SessionContainerInterface; - options: APIOptions; - userContext: UserContext; - }): Promise< - | { - status: "OK"; - session: SessionContainerInterface; - user: User; - } - | { - status: "WRONG_CREDENTIALS_ERROR"; - } - | { - status: "SIGN_IN_NOT_ALLOWED"; - reason: string; - } - | GeneralErrorResponse - > { + }) { const errorCodeMap = { SIGN_IN_NOT_ALLOWED: "Cannot sign in due to security reasons. Please try resetting your password, use a different login method or contact support. (ERR_CODE_008)", @@ -683,6 +660,7 @@ export default function getAPIImplementation(): APIInterface { tenantId, userContext, session, + shouldTryLinkingWithSessionUser, }); if (preAuthChecks.status === "SIGN_UP_NOT_ALLOWED") { throw new Error("This should never happen: pre-auth checks should not fail for sign in"); @@ -702,6 +680,7 @@ export default function getAPIImplementation(): APIInterface { email, password, session, + shouldTryLinkingWithSessionUser, tenantId, userContext, }); @@ -740,32 +719,10 @@ export default function getAPIImplementation(): APIInterface { formFields, tenantId, session, + shouldTryLinkingWithSessionUser, options, userContext, - }: { - formFields: { - id: string; - value: string; - }[]; - tenantId: string; - session?: SessionContainerInterface; - options: APIOptions; - userContext: UserContext; - }): Promise< - | { - status: "OK"; - session: SessionContainerInterface; - user: User; - } - | { - status: "SIGN_UP_NOT_ALLOWED"; - reason: string; - } - | { - status: "EMAIL_ALREADY_EXISTS_ERROR"; - } - | GeneralErrorResponse - > { + }) { const errorCodeMap = { SIGN_UP_NOT_ALLOWED: "Cannot sign up due to security reasons. Please try logging in, use a different login method or contact support. (ERR_CODE_007)", @@ -797,6 +754,7 @@ export default function getAPIImplementation(): APIInterface { tenantId, userContext, session, + shouldTryLinkingWithSessionUser, }); if (preAuthCheckRes.status === "SIGN_UP_NOT_ALLOWED") { @@ -834,6 +792,7 @@ export default function getAPIImplementation(): APIInterface { email, password, session, + shouldTryLinkingWithSessionUser, userContext, }); diff --git a/lib/ts/recipe/emailpassword/api/signin.ts b/lib/ts/recipe/emailpassword/api/signin.ts index 77c1161ff..2899715cf 100644 --- a/lib/ts/recipe/emailpassword/api/signin.ts +++ b/lib/ts/recipe/emailpassword/api/signin.ts @@ -13,11 +13,15 @@ * under the License. */ -import { getBackwardsCompatibleUserInfo, send200Response } from "../../../utils"; +import { + getBackwardsCompatibleUserInfo, + getNormalisedShouldTryLinkingWithSessionUserFlag, + send200Response, +} from "../../../utils"; import { validateFormFieldsOrThrowError } from "./utils"; import { APIInterface, APIOptions } from "../"; import { UserContext } from "../../../types"; -import Session from "../../session"; +import { AuthUtils } from "../../../authUtils"; export default async function signInAPI( apiImplementation: APIInterface, @@ -30,24 +34,24 @@ export default async function signInAPI( return false; } + const body = await options.req.getJSONBody(); // step 1 let formFields: { id: string; value: string; }[] = await validateFormFieldsOrThrowError( options.config.signInFeature.formFields, - (await options.req.getJSONBody()).formFields, + body.formFields, tenantId, userContext ); - let session = await Session.getSession( + const shouldTryLinkingWithSessionUser = getNormalisedShouldTryLinkingWithSessionUserFlag(options.req, body); + + const session = await AuthUtils.loadSessionInAuthAPIIfNeeded( options.req, options.res, - { - sessionRequired: false, - overrideGlobalClaimValidators: () => [], - }, + shouldTryLinkingWithSessionUser, userContext ); @@ -59,6 +63,7 @@ export default async function signInAPI( formFields, tenantId, session, + shouldTryLinkingWithSessionUser, options, userContext, }); diff --git a/lib/ts/recipe/emailpassword/api/signup.ts b/lib/ts/recipe/emailpassword/api/signup.ts index ec9a2781d..67a4b49e1 100644 --- a/lib/ts/recipe/emailpassword/api/signup.ts +++ b/lib/ts/recipe/emailpassword/api/signup.ts @@ -13,12 +13,16 @@ * under the License. */ -import { getBackwardsCompatibleUserInfo, send200Response } from "../../../utils"; +import { + getBackwardsCompatibleUserInfo, + getNormalisedShouldTryLinkingWithSessionUserFlag, + send200Response, +} from "../../../utils"; import { validateFormFieldsOrThrowError } from "./utils"; import { APIInterface, APIOptions } from "../"; import STError from "../error"; import { UserContext } from "../../../types"; -import Session from "../../session"; +import { AuthUtils } from "../../../authUtils"; export default async function signUpAPI( apiImplementation: APIInterface, @@ -45,16 +49,14 @@ export default async function signUpAPI( userContext ); - let session = await Session.getSession( + const shouldTryLinkingWithSessionUser = getNormalisedShouldTryLinkingWithSessionUserFlag(options.req, requestBody); + + const session = await AuthUtils.loadSessionInAuthAPIIfNeeded( options.req, options.res, - { - sessionRequired: false, - overrideGlobalClaimValidators: () => [], - }, + shouldTryLinkingWithSessionUser, userContext ); - if (session !== undefined) { tenantId = session.getTenantId(); } @@ -63,6 +65,7 @@ export default async function signUpAPI( formFields, tenantId, session, + shouldTryLinkingWithSessionUser, options, userContext: userContext, }); diff --git a/lib/ts/recipe/emailpassword/index.ts b/lib/ts/recipe/emailpassword/index.ts index e654d16b4..d5b071b51 100644 --- a/lib/ts/recipe/emailpassword/index.ts +++ b/lib/ts/recipe/emailpassword/index.ts @@ -91,6 +91,7 @@ export default class Wrapper { email, password, session, + shouldTryLinkingWithSessionUser: !!session, tenantId: tenantId === undefined ? DEFAULT_TENANT_ID : tenantId, userContext: getUserContext(userContext), }); @@ -143,6 +144,7 @@ export default class Wrapper { email, password, session, + shouldTryLinkingWithSessionUser: !!session, tenantId: tenantId === undefined ? DEFAULT_TENANT_ID : tenantId, userContext: getUserContext(userContext), }); diff --git a/lib/ts/recipe/emailpassword/recipeImplementation.ts b/lib/ts/recipe/emailpassword/recipeImplementation.ts index 4c2e39d70..1c883021c 100644 --- a/lib/ts/recipe/emailpassword/recipeImplementation.ts +++ b/lib/ts/recipe/emailpassword/recipeImplementation.ts @@ -18,7 +18,7 @@ export default function getRecipeInterface( return { signUp: async function ( this: RecipeInterface, - { email, password, tenantId, session, userContext } + { email, password, tenantId, session, shouldTryLinkingWithSessionUser, userContext } ): Promise< | { status: "OK"; @@ -47,11 +47,12 @@ export default function getRecipeInterface( let updatedUser = response.user; - const linkResult = await AuthUtils.linkToSessionIfProvidedElseCreatePrimaryUserIdOrLinkByAccountInfo({ + const linkResult = await AuthUtils.linkToSessionIfRequiredElseCreatePrimaryUserIdOrLinkByAccountInfo({ tenantId, inputUser: response.user, recipeUserId: response.recipeUserId, session, + shouldTryLinkingWithSessionUser, userContext, }); @@ -103,7 +104,10 @@ export default function getRecipeInterface( // users are always initially unverified. }, - signIn: async function (this: RecipeInterface, { email, password, tenantId, session, userContext }) { + signIn: async function ( + this: RecipeInterface, + { email, password, tenantId, session, shouldTryLinkingWithSessionUser, userContext } + ) { const response = await this.verifyCredentials({ email, password, tenantId, userContext }); if (response.status === "OK") { @@ -133,11 +137,12 @@ export default function getRecipeInterface( response.user = (await getUser(response.recipeUserId!.getAsString(), userContext))!; } - const linkResult = await AuthUtils.linkToSessionIfProvidedElseCreatePrimaryUserIdOrLinkByAccountInfo({ + const linkResult = await AuthUtils.linkToSessionIfRequiredElseCreatePrimaryUserIdOrLinkByAccountInfo({ tenantId, inputUser: response.user, recipeUserId: response.recipeUserId, session, + shouldTryLinkingWithSessionUser, userContext, }); if (linkResult.status === "LINKING_TO_SESSION_USER_FAILED") { @@ -320,6 +325,7 @@ export default function getRecipeInterface( email: input.email, password: input.password, }, + {}, input.userContext ); diff --git a/lib/ts/recipe/emailpassword/types.ts b/lib/ts/recipe/emailpassword/types.ts index ee8b481df..563c95d4f 100644 --- a/lib/ts/recipe/emailpassword/types.ts +++ b/lib/ts/recipe/emailpassword/types.ts @@ -88,6 +88,7 @@ export type RecipeInterface = { email: string; password: string; session: SessionContainerInterface | undefined; + shouldTryLinkingWithSessionUser: boolean | undefined; tenantId: string; userContext: UserContext; }): Promise< @@ -128,6 +129,7 @@ export type RecipeInterface = { email: string; password: string; session: SessionContainerInterface | undefined; + shouldTryLinkingWithSessionUser: boolean | undefined; tenantId: string; userContext: UserContext; }): Promise< @@ -277,6 +279,7 @@ export type APIInterface = { }[]; tenantId: string; session: SessionContainerInterface | undefined; + shouldTryLinkingWithSessionUser: boolean | undefined; options: APIOptions; userContext: UserContext; }) => Promise< @@ -304,6 +307,7 @@ export type APIInterface = { }[]; tenantId: string; session: SessionContainerInterface | undefined; + shouldTryLinkingWithSessionUser: boolean | undefined; options: APIOptions; userContext: UserContext; }) => Promise< diff --git a/lib/ts/recipe/jwt/api/implementation.ts b/lib/ts/recipe/jwt/api/implementation.ts index 4308c7cf1..03bcab7ec 100644 --- a/lib/ts/recipe/jwt/api/implementation.ts +++ b/lib/ts/recipe/jwt/api/implementation.ts @@ -31,6 +31,17 @@ export default function getAPIImplementation(): APIInterface { options.res.setHeader("Cache-Control", `max-age=${resp.validityInSeconds}, must-revalidate`, false); } + const oauth2Provider = require("../../oauth2provider/recipe").default.getInstance(); + + // TODO: dirty hack until we get core support + if (oauth2Provider !== undefined) { + const oauth2JWKSRes = await fetch("http://localhost:4444/.well-known/jwks.json"); + if (oauth2JWKSRes.ok) { + const oauth2RespBody = await oauth2JWKSRes.json(); + resp.keys = resp.keys.concat(oauth2RespBody.keys); + } + } + return { keys: resp.keys, }; diff --git a/lib/ts/recipe/jwt/recipeImplementation.ts b/lib/ts/recipe/jwt/recipeImplementation.ts index dc8656124..fa937c881 100644 --- a/lib/ts/recipe/jwt/recipeImplementation.ts +++ b/lib/ts/recipe/jwt/recipeImplementation.ts @@ -78,6 +78,7 @@ export default function getRecipeInterface( const { body, headers } = await querier.sendGetRequestWithResponseHeaders( new NormalisedURLPath("/.well-known/jwks.json"), {}, + undefined, userContext ); let validityInSeconds = defaultJWKSMaxAge; diff --git a/lib/ts/recipe/multifactorauth/multiFactorAuthClaim.ts b/lib/ts/recipe/multifactorauth/multiFactorAuthClaim.ts index 2d7fd36a0..70dd5ed50 100644 --- a/lib/ts/recipe/multifactorauth/multiFactorAuthClaim.ts +++ b/lib/ts/recipe/multifactorauth/multiFactorAuthClaim.ts @@ -232,8 +232,9 @@ export class MultiFactorAuthClaimClass extends SessionClaim { return retVal; }; - public removeFromPayloadByMerge_internal = () => { + public removeFromPayloadByMerge_internal = (payload: JSONObject) => { return { + ...payload, [this.key]: null, }; }; diff --git a/lib/ts/recipe/multitenancy/recipeImplementation.ts b/lib/ts/recipe/multitenancy/recipeImplementation.ts index 48316c97e..d3694647a 100644 --- a/lib/ts/recipe/multitenancy/recipeImplementation.ts +++ b/lib/ts/recipe/multitenancy/recipeImplementation.ts @@ -16,6 +16,7 @@ export default function getRecipeInterface(querier: Querier): RecipeInterface { tenantId, ...config, }, + {}, userContext ); @@ -68,6 +69,7 @@ export default function getRecipeInterface(querier: Querier): RecipeInterface { config, skipValidation, }, + {}, userContext ); return response; diff --git a/lib/ts/recipe/oauth2client/api/implementation.ts b/lib/ts/recipe/oauth2client/api/implementation.ts new file mode 100644 index 000000000..7e18629e0 --- /dev/null +++ b/lib/ts/recipe/oauth2client/api/implementation.ts @@ -0,0 +1,59 @@ +import { APIInterface } from "../"; +import Session from "../../session"; +import { OAuthTokens } from "../types"; + +export default function getAPIInterface(): APIInterface { + return { + signInPOST: async function (input) { + const { options, tenantId, userContext } = input; + + const providerConfig = await options.recipeImplementation.getProviderConfig({ userContext }); + + let oAuthTokensToUse: OAuthTokens = {}; + + if ("redirectURIInfo" in input && input.redirectURIInfo !== undefined) { + oAuthTokensToUse = await options.recipeImplementation.exchangeAuthCodeForOAuthTokens({ + providerConfig, + redirectURIInfo: input.redirectURIInfo, + userContext, + }); + } else if ("oAuthTokens" in input && input.oAuthTokens !== undefined) { + oAuthTokensToUse = input.oAuthTokens; + } else { + throw Error("should never come here"); + } + + const { userId, rawUserInfo } = await options.recipeImplementation.getUserInfo({ + providerConfig, + oAuthTokens: oAuthTokensToUse, + userContext, + }); + + const { user, recipeUserId } = await options.recipeImplementation.signIn({ + userId, + tenantId, + rawUserInfo, + oAuthTokens: oAuthTokensToUse, + userContext, + }); + + const session = await Session.createNewSession( + options.req, + options.res, + tenantId, + recipeUserId, + undefined, + undefined, + userContext + ); + + return { + status: "OK", + user, + session, + oAuthTokens: oAuthTokensToUse, + rawUserInfo, + }; + }, + }; +} diff --git a/lib/ts/recipe/oauth2client/api/signin.ts b/lib/ts/recipe/oauth2client/api/signin.ts new file mode 100644 index 000000000..6e89436f9 --- /dev/null +++ b/lib/ts/recipe/oauth2client/api/signin.ts @@ -0,0 +1,91 @@ +/* Copyright (c) 2024, 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"; +import { getBackwardsCompatibleUserInfo, send200Response } from "../../../utils"; +import { APIInterface, APIOptions } from ".."; +import { UserContext } from "../../../types"; +import Session from "../../session"; + +export default async function signInAPI( + apiImplementation: APIInterface, + tenantId: string, + options: APIOptions, + userContext: UserContext +): Promise { + if (apiImplementation.signInPOST === undefined) { + return false; + } + + const bodyParams = await options.req.getJSONBody(); + + let redirectURIInfo: + | undefined + | { + redirectURI: string; + redirectURIQueryParams: any; + pkceCodeVerifier?: string; + }; + let oAuthTokens: any; + + if (bodyParams.redirectURIInfo !== undefined) { + if (bodyParams.redirectURIInfo.redirectURI === undefined) { + throw new STError({ + type: STError.BAD_INPUT_ERROR, + message: "Please provide the redirectURI in request body", + }); + } + redirectURIInfo = bodyParams.redirectURIInfo; + } else if (bodyParams.oAuthTokens !== undefined) { + oAuthTokens = bodyParams.oAuthTokens; + } else { + throw new STError({ + type: STError.BAD_INPUT_ERROR, + message: "Please provide one of redirectURIInfo or oAuthTokens in the request body", + }); + } + + let session = await Session.getSession( + options.req, + options.res, + { + sessionRequired: false, + overrideGlobalClaimValidators: () => [], + }, + userContext + ); + + if (session !== undefined) { + tenantId = session.getTenantId(); + } + + let result = await apiImplementation.signInPOST({ + tenantId, + redirectURIInfo, + oAuthTokens, + options, + userContext, + }); + + if (result.status === "OK") { + send200Response(options.res, { + status: result.status, + ...getBackwardsCompatibleUserInfo(options.req, result, userContext), + }); + } else { + send200Response(options.res, result); + } + return true; +} diff --git a/lib/ts/recipe/oauth2client/constants.ts b/lib/ts/recipe/oauth2client/constants.ts new file mode 100644 index 000000000..8e45f0567 --- /dev/null +++ b/lib/ts/recipe/oauth2client/constants.ts @@ -0,0 +1,16 @@ +/* Copyright (c) 2024, 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. + */ + +export const SIGN_IN_API = "/oauth/client/signin"; diff --git a/lib/ts/recipe/oauth2client/index.ts b/lib/ts/recipe/oauth2client/index.ts new file mode 100644 index 000000000..d2b2e2a02 --- /dev/null +++ b/lib/ts/recipe/oauth2client/index.ts @@ -0,0 +1,63 @@ +/* Copyright (c) 2024, 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 { getUserContext } from "../../utils"; +import Recipe from "./recipe"; +import { RecipeInterface, APIInterface, APIOptions, OAuthTokens } from "./types"; + +export default class Wrapper { + static init = Recipe.init; + + static async exchangeAuthCodeForOAuthTokens( + redirectURIInfo: { + redirectURI: string; + redirectURIQueryParams: any; + pkceCodeVerifier?: string | undefined; + }, + userContext?: Record + ) { + const recipeInterfaceImpl = Recipe.getInstanceOrThrowError().recipeInterfaceImpl; + const normalisedUserContext = getUserContext(userContext); + const providerConfig = await recipeInterfaceImpl.getProviderConfig({ + userContext: normalisedUserContext, + }); + return await recipeInterfaceImpl.exchangeAuthCodeForOAuthTokens({ + providerConfig, + redirectURIInfo, + userContext: normalisedUserContext, + }); + } + + static async getUserInfo(oAuthTokens: OAuthTokens, userContext?: Record) { + const recipeInterfaceImpl = Recipe.getInstanceOrThrowError().recipeInterfaceImpl; + const normalisedUserContext = getUserContext(userContext); + const providerConfig = await recipeInterfaceImpl.getProviderConfig({ + userContext: normalisedUserContext, + }); + return await Recipe.getInstanceOrThrowError().recipeInterfaceImpl.getUserInfo({ + providerConfig, + oAuthTokens, + userContext: normalisedUserContext, + }); + } +} + +export let init = Wrapper.init; + +export let exchangeAuthCodeForOAuthTokens = Wrapper.exchangeAuthCodeForOAuthTokens; + +export let getUserInfo = Wrapper.getUserInfo; + +export type { RecipeInterface, APIInterface, APIOptions }; diff --git a/lib/ts/recipe/oauth2client/recipe.ts b/lib/ts/recipe/oauth2client/recipe.ts new file mode 100644 index 000000000..7da475c5c --- /dev/null +++ b/lib/ts/recipe/oauth2client/recipe.ts @@ -0,0 +1,137 @@ +/* Copyright (c) 2024, 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 RecipeModule from "../../recipeModule"; +import { NormalisedAppinfo, APIHandled, RecipeListFunction, HTTPMethod, UserContext } from "../../types"; +import { TypeInput, TypeNormalisedInput, RecipeInterface, APIInterface } from "./types"; +import { validateAndNormaliseUserInput } from "./utils"; +import STError from "../../error"; +import { SIGN_IN_API } from "./constants"; +import NormalisedURLPath from "../../normalisedURLPath"; +import signInAPI from "./api/signin"; +import RecipeImplementation from "./recipeImplementation"; +import APIImplementation from "./api/implementation"; +import { Querier } from "../../querier"; +import type { BaseRequest, BaseResponse } from "../../framework"; +import OverrideableBuilder from "supertokens-js-override"; + +export default class Recipe extends RecipeModule { + private static instance: Recipe | undefined = undefined; + static RECIPE_ID = "oauth2client"; + + config: TypeNormalisedInput; + + recipeInterfaceImpl: RecipeInterface; + + apiImpl: APIInterface; + + isInServerlessEnv: boolean; + + constructor( + recipeId: string, + appInfo: NormalisedAppinfo, + isInServerlessEnv: boolean, + config: TypeInput, + _recipes: {} + ) { + super(recipeId, appInfo); + this.config = validateAndNormaliseUserInput(appInfo, config); + this.isInServerlessEnv = isInServerlessEnv; + + { + let builder = new OverrideableBuilder( + RecipeImplementation(Querier.getNewInstanceOrThrowError(recipeId), this.config) + ); + this.recipeInterfaceImpl = builder.override(this.config.override.functions).build(); + } + { + let builder = new OverrideableBuilder(APIImplementation()); + this.apiImpl = builder.override(this.config.override.apis).build(); + } + } + + static init(config: TypeInput): RecipeListFunction { + return (appInfo, isInServerlessEnv) => { + if (Recipe.instance === undefined) { + Recipe.instance = new Recipe(Recipe.RECIPE_ID, appInfo, isInServerlessEnv, config, {}); + + return Recipe.instance; + } else { + throw new Error("OAuth2Client recipe has already been initialised. Please check your code for bugs."); + } + }; + } + + static getInstanceOrThrowError(): Recipe { + if (Recipe.instance !== undefined) { + return Recipe.instance; + } + throw new Error("Initialisation not done. Did you forget to call the OAuth2Client.init function?"); + } + + static reset() { + if (process.env.TEST_MODE !== "testing") { + throw new Error("calling testing function in non testing env"); + } + Recipe.instance = undefined; + } + + getAPIsHandled = (): APIHandled[] => { + return [ + { + method: "post", + pathWithoutApiBasePath: new NormalisedURLPath(SIGN_IN_API), + id: SIGN_IN_API, + disabled: this.apiImpl.signInPOST === undefined, + }, + ]; + }; + + handleAPIRequest = async ( + id: string, + tenantId: string, + req: BaseRequest, + res: BaseResponse, + _path: NormalisedURLPath, + _method: HTTPMethod, + userContext: UserContext + ): Promise => { + let options = { + config: this.config, + recipeId: this.getRecipeId(), + isInServerlessEnv: this.isInServerlessEnv, + recipeImplementation: this.recipeInterfaceImpl, + req, + res, + appInfo: this.getAppInfo(), + }; + if (id === SIGN_IN_API) { + return await signInAPI(this.apiImpl, tenantId, options, userContext); + } + return false; + }; + + handleError = async (err: STError, _request: BaseRequest, _response: BaseResponse): Promise => { + throw err; + }; + + getAllCORSHeaders = (): string[] => { + return []; + }; + + isErrorFromThisRecipe = (err: any): err is STError => { + return STError.isErrorFromSuperTokens(err) && err.fromRecipe === Recipe.RECIPE_ID; + }; +} diff --git a/lib/ts/recipe/oauth2client/recipeImplementation.ts b/lib/ts/recipe/oauth2client/recipeImplementation.ts new file mode 100644 index 000000000..a5115c815 --- /dev/null +++ b/lib/ts/recipe/oauth2client/recipeImplementation.ts @@ -0,0 +1,181 @@ +import { + OAuthTokenResponse, + OAuthTokens, + ProviderConfigWithOIDCInfo, + RecipeInterface, + TypeNormalisedInput, + UserInfo, +} from "./types"; +import { Querier } from "../../querier"; +import RecipeUserId from "../../recipeUserId"; +import { User as UserType } from "../../types"; +import { + doGetRequest, + doPostRequest, + getOIDCDiscoveryInfo, + verifyIdTokenFromJWKSEndpointAndGetPayload, +} from "../../thirdpartyUtils"; +import { getUser } from "../.."; +import { logDebugMessage } from "../../logger"; +import { JWTVerifyGetKey, createRemoteJWKSet } from "jose"; + +export default function getRecipeImplementation(_querier: Querier, config: TypeNormalisedInput): RecipeInterface { + let providerConfigWithOIDCInfo: ProviderConfigWithOIDCInfo | null = null; + + return { + signIn: async function ({ + userId, + tenantId, + userContext, + oAuthTokens, + rawUserInfo, + }): Promise<{ + status: "OK"; + user: UserType; + recipeUserId: RecipeUserId; + oAuthTokens: OAuthTokens; + rawUserInfo: { + fromIdTokenPayload?: { [key: string]: any }; + fromUserInfoAPI?: { [key: string]: any }; + }; + }> { + const user = await getUser(userId, userContext); + + if (user === undefined) { + throw new Error(`Failed to getUser from the userId ${userId} in the ${tenantId} tenant`); + } + + return { + status: "OK", + user, + recipeUserId: new RecipeUserId(userId), + oAuthTokens, + rawUserInfo, + }; + }, + getProviderConfig: async function () { + if (providerConfigWithOIDCInfo !== null) { + return providerConfigWithOIDCInfo; + } + const oidcInfo = await getOIDCDiscoveryInfo(config.providerConfig.oidcDiscoveryEndpoint); + + if (oidcInfo.authorization_endpoint === undefined) { + throw new Error("Failed to authorization_endpoint from the oidcDiscoveryEndpoint."); + } + if (oidcInfo.token_endpoint === undefined) { + throw new Error("Failed to token_endpoint from the oidcDiscoveryEndpoint."); + } + if (oidcInfo.userinfo_endpoint === undefined) { + throw new Error("Failed to userinfo_endpoint from the oidcDiscoveryEndpoint."); + } + if (oidcInfo.jwks_uri === undefined) { + throw new Error("Failed to jwks_uri from the oidcDiscoveryEndpoint."); + } + + providerConfigWithOIDCInfo = { + ...config.providerConfig, + authorizationEndpoint: oidcInfo.authorization_endpoint, + tokenEndpoint: oidcInfo.token_endpoint, + userInfoEndpoint: oidcInfo.userinfo_endpoint, + jwksURI: oidcInfo.jwks_uri, + }; + return providerConfigWithOIDCInfo; + }, + exchangeAuthCodeForOAuthTokens: async function (this: RecipeInterface, { providerConfig, redirectURIInfo }) { + if (providerConfig.tokenEndpoint === undefined) { + throw new Error("OAuth2Client provider's tokenEndpoint is not configured."); + } + const tokenAPIURL = providerConfig.tokenEndpoint; + const accessTokenAPIParams: { [key: string]: string } = { + client_id: providerConfig.clientId, + redirect_uri: redirectURIInfo.redirectURI, + code: redirectURIInfo.redirectURIQueryParams["code"], + grant_type: "authorization_code", + }; + if (providerConfig.clientSecret !== undefined) { + accessTokenAPIParams["client_secret"] = providerConfig.clientSecret; + } + if (redirectURIInfo.pkceCodeVerifier !== undefined) { + accessTokenAPIParams["code_verifier"] = redirectURIInfo.pkceCodeVerifier; + } + + const tokenResponse = await doPostRequest(tokenAPIURL, accessTokenAPIParams); + + if (tokenResponse.status >= 400) { + logDebugMessage( + `Received response with status ${tokenResponse.status} and body ${tokenResponse.stringResponse}` + ); + throw new Error( + `Received response with status ${tokenResponse.status} and body ${tokenResponse.stringResponse}` + ); + } + + return tokenResponse.jsonResponse as OAuthTokenResponse; + }, + getUserInfo: async function ({ providerConfig, oAuthTokens }): Promise { + let jwks: JWTVerifyGetKey | undefined; + + const accessToken = oAuthTokens["access_token"]; + const idToken = oAuthTokens["id_token"]; + + let rawUserInfo: { + fromUserInfoAPI: any; + fromIdTokenPayload: any; + } = { + fromUserInfoAPI: {}, + fromIdTokenPayload: {}, + }; + + if (idToken && providerConfig.jwksURI !== undefined) { + if (jwks === undefined) { + jwks = createRemoteJWKSet(new URL(providerConfig.jwksURI)); + } + + rawUserInfo.fromIdTokenPayload = await verifyIdTokenFromJWKSEndpointAndGetPayload(idToken, jwks, { + audience: providerConfig.clientId, + }); + } + + if (accessToken && providerConfig.userInfoEndpoint !== undefined) { + const headers: { [key: string]: string } = { + Authorization: "Bearer " + accessToken, + }; + const queryParams: { [key: string]: string } = {}; + + const userInfoFromAccessToken = await doGetRequest( + providerConfig.userInfoEndpoint, + queryParams, + headers + ); + + if (userInfoFromAccessToken.status >= 400) { + logDebugMessage( + `Received response with status ${userInfoFromAccessToken.status} and body ${userInfoFromAccessToken.stringResponse}` + ); + throw new Error( + `Received response with status ${userInfoFromAccessToken.status} and body ${userInfoFromAccessToken.stringResponse}` + ); + } + + rawUserInfo.fromUserInfoAPI = userInfoFromAccessToken.jsonResponse; + } + + let userId: string | undefined = undefined; + + if (rawUserInfo.fromIdTokenPayload?.sub !== undefined) { + userId = rawUserInfo.fromIdTokenPayload["sub"]; + } else if (rawUserInfo.fromUserInfoAPI?.sub !== undefined) { + userId = rawUserInfo.fromUserInfoAPI["sub"]; + } + + if (userId === undefined) { + throw new Error(`Failed to get userId from both the idToken and userInfo endpoint.`); + } + + return { + userId, + rawUserInfo, + }; + }, + }; +} diff --git a/lib/ts/recipe/oauth2client/types.ts b/lib/ts/recipe/oauth2client/types.ts new file mode 100644 index 000000000..740d00d16 --- /dev/null +++ b/lib/ts/recipe/oauth2client/types.ts @@ -0,0 +1,156 @@ +/* Copyright (c) 2024, 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 type { BaseRequest, BaseResponse } from "../../framework"; +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"; + +export type UserInfo = { + userId: string; + rawUserInfo: { fromIdTokenPayload?: { [key: string]: any }; fromUserInfoAPI?: { [key: string]: any } }; +}; + +export type ProviderConfigInput = { + clientId: string; + clientSecret?: string; + oidcDiscoveryEndpoint: string; +}; + +export type ProviderConfigWithOIDCInfo = ProviderConfigInput & { + authorizationEndpoint: string; + tokenEndpoint: string; + userInfoEndpoint: string; + jwksURI: string; +}; + +export type OAuthTokens = { + access_token?: string; + id_token?: string; +}; + +export type OAuthTokenResponse = { + access_token: string; + id_token?: string; + refresh_token?: string; + expires_in: number; + scope?: string; + token_type: string; +}; + +export type TypeInput = { + providerConfig: ProviderConfigInput; + override?: { + functions?: ( + originalImplementation: RecipeInterface, + builder?: OverrideableBuilder + ) => RecipeInterface; + apis?: (originalImplementation: APIInterface, builder?: OverrideableBuilder) => APIInterface; + }; +}; + +export type TypeNormalisedInput = { + providerConfig: ProviderConfigInput; + override: { + functions: ( + originalImplementation: RecipeInterface, + builder?: OverrideableBuilder + ) => RecipeInterface; + apis: (originalImplementation: APIInterface, builder?: OverrideableBuilder) => APIInterface; + }; +}; + +export type RecipeInterface = { + getProviderConfig(input: { userContext: UserContext }): Promise; + + signIn(input: { + userId: string; + oAuthTokens: OAuthTokens; + rawUserInfo: { + fromIdTokenPayload?: { [key: string]: any }; + fromUserInfoAPI?: { [key: string]: any }; + }; + tenantId: string; + userContext: UserContext; + }): Promise<{ + status: "OK"; + recipeUserId: RecipeUserId; + user: User; + oAuthTokens: OAuthTokens; + rawUserInfo: { + fromIdTokenPayload?: { [key: string]: any }; + fromUserInfoAPI?: { [key: string]: any }; + }; + }>; + exchangeAuthCodeForOAuthTokens(input: { + providerConfig: ProviderConfigWithOIDCInfo; + redirectURIInfo: { + redirectURI: string; + redirectURIQueryParams: any; + pkceCodeVerifier?: string | undefined; + }; + userContext: UserContext; + }): Promise; + getUserInfo(input: { + providerConfig: ProviderConfigWithOIDCInfo; + oAuthTokens: OAuthTokens; + userContext: UserContext; + }): Promise; +}; + +export type APIOptions = { + recipeImplementation: RecipeInterface; + config: TypeNormalisedInput; + recipeId: string; + isInServerlessEnv: boolean; + req: BaseRequest; + res: BaseResponse; + appInfo: NormalisedAppinfo; +}; + +export type APIInterface = { + signInPOST: ( + input: { + tenantId: string; + options: APIOptions; + userContext: UserContext; + } & ( + | { + redirectURIInfo: { + redirectURI: string; + redirectURIQueryParams: any; + pkceCodeVerifier?: string; + }; + } + | { + oAuthTokens: { [key: string]: any }; + } + ) + ) => Promise< + | { + status: "OK"; + user: User; + session: SessionContainerInterface; + oAuthTokens: { [key: string]: any }; + rawUserInfo: { + fromIdTokenPayload?: { [key: string]: any }; + fromUserInfoAPI?: { [key: string]: any }; + }; + } + | GeneralErrorResponse + >; +}; diff --git a/lib/ts/recipe/oauth2client/utils.ts b/lib/ts/recipe/oauth2client/utils.ts new file mode 100644 index 000000000..5996c2490 --- /dev/null +++ b/lib/ts/recipe/oauth2client/utils.ts @@ -0,0 +1,49 @@ +/* Copyright (c) 2024, 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 { NormalisedAppinfo } from "../../types"; +import { TypeInput, TypeNormalisedInput, RecipeInterface, APIInterface } from "./types"; + +export function validateAndNormaliseUserInput(_appInfo: NormalisedAppinfo, config: TypeInput): TypeNormalisedInput { + if (config === undefined || config.providerConfig === undefined) { + throw new Error("Please pass providerConfig argument in the OAuth2Client recipe."); + } + + if (config.providerConfig.clientId === undefined) { + throw new Error("Please pass clientId argument in the OAuth2Client providerConfig."); + } + + // TODO: Decide on the prefix and also if we will allow users to customise clientIds + // if (!config.providerConfig.clientId.startsWith("supertokens_")) { + // throw new Error( + // `Only Supertokens OAuth ClientIds are supported in the OAuth2Client recipe. For any other OAuth Clients use the thirdparty recipe.` + // ); + // } + + if (config.providerConfig.oidcDiscoveryEndpoint === undefined) { + throw new Error("Please pass oidcDiscoveryEndpoint argument in the OAuth2Client providerConfig."); + } + + let override = { + functions: (originalImplementation: RecipeInterface) => originalImplementation, + apis: (originalImplementation: APIInterface) => originalImplementation, + ...config?.override, + }; + + return { + providerConfig: config.providerConfig, + override, + }; +} diff --git a/lib/ts/recipe/oauth2provider/OAuth2Client.ts b/lib/ts/recipe/oauth2provider/OAuth2Client.ts new file mode 100644 index 000000000..7105a9262 --- /dev/null +++ b/lib/ts/recipe/oauth2provider/OAuth2Client.ts @@ -0,0 +1,242 @@ +/* Copyright (c) 2024, 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 { transformObjectKeys } from "../../utils"; +import { OAuth2ClientOptions } from "./types"; + +export class OAuth2Client { + /** + * OAuth 2.0 Client ID + * The ID is immutable. If no ID is provided, a UUID4 will be generated. + */ + clientId: string; + + /** + * OAuth 2.0 Client Secret + * The secret will be included in the create request as cleartext, and then + * never again. The secret is kept in hashed format and is not recoverable once lost. + */ + clientSecret?: string; + + /** + * OAuth 2.0 Client Name + * The human-readable name of the client to be presented to the end-user during authorization. + */ + clientName: string; + + /** + * OAuth 2.0 Client Scope + * Scope is a string containing a space-separated list of scope values that the client + * can use when requesting access tokens. + */ + scope: string; + + /** + * Array of redirect URIs + * StringSliceJSONFormat represents []string{} which is encoded to/from JSON for SQL storage. + */ + redirectUris: string[] | null; + + /** + * Authorization Code Grant Access Token Lifespan + * NullDuration - ^[0-9]+(ns|us|ms|s|m|h)$ + */ + authorizationCodeGrantAccessTokenLifespan: string | null; + + /** + * Authorization Code Grant ID Token Lifespan + * NullDuration - ^[0-9]+(ns|us|ms|s|m|h)$ + */ + authorizationCodeGrantIdTokenLifespan: string | null; + + /** + * Authorization Code Grant Refresh Token Lifespan + * NullDuration - ^[0-9]+(ns|us|ms|s|m|h)$ + */ + authorizationCodeGrantRefreshTokenLifespan: string | null; + + /** + * Client Credentials Grant Access Token Lifespan + * NullDuration - ^[0-9]+(ns|us|ms|s|m|h)$ + */ + clientCredentialsGrantAccessTokenLifespan: string | null; + + /** + * Implicit Grant Access Token Lifespan + * NullDuration - ^[0-9]+(ns|us|ms|s|m|h)$ + */ + implicitGrantAccessTokenLifespan: string | null; + + /** + * Implicit Grant ID Token Lifespan + * NullDuration - ^[0-9]+(ns|us|ms|s|m|h)$ + */ + implicitGrantIdTokenLifespan: string | null; + + /** + * Refresh Token Grant Access Token Lifespan + * NullDuration - ^[0-9]+(ns|us|ms|s|m|h)$ + */ + refreshTokenGrantAccessTokenLifespan: string | null; + + /** + * Refresh Token Grant ID Token Lifespan + * NullDuration - ^[0-9]+(ns|us|ms|s|m|h)$ + */ + refreshTokenGrantIdTokenLifespan: string | null; + + /** + * Refresh Token Grant Refresh Token Lifespan + * NullDuration - ^[0-9]+(ns|us|ms|s|m|h)$ + */ + refreshTokenGrantRefreshTokenLifespan: string | null; + + /** + * OAuth 2.0 Token Endpoint Authentication Method + * Requested Client Authentication method for the Token Endpoint. + */ + tokenEndpointAuthMethod: string; + + /** + * OAuth 2.0 Client URI + * ClientURI is a URL string of a web page providing information about the client. + */ + clientUri: string; + + /** + * Array of allowed CORS origins + * StringSliceJSONFormat represents []string{} which is encoded to/from JSON for SQL storage. + */ + allowedCorsOrigins: string[]; + + /** + * Array of audiences + * StringSliceJSONFormat represents []string{} which is encoded to/from JSON for SQL storage. + */ + audience: string[]; + + /** + * Array of grant types + * StringSliceJSONFormat represents []string{} which is encoded to/from JSON for SQL storage. + */ + grantTypes: string[] | null; + + /** + * Array of response types + * StringSliceJSONFormat represents []string{} which is encoded to/from JSON for SQL storage. + */ + responseTypes: string[] | null; + + /** + * OAuth 2.0 Client Logo URI + * A URL string referencing the client's logo. + */ + logoUri: string; + + /** + * OAuth 2.0 Client Policy URI + * PolicyURI is a URL string that points to a human-readable privacy policy document + * that describes how the deployment organization collects, uses, + * retains, and discloses personal data. + */ + policyUri: string; + + /** + * OAuth 2.0 Client Terms of Service URI + * A URL string pointing to a human-readable terms of service + * document for the client that describes a contractual relationship + * between the end-user and the client that the end-user accepts when + * authorizing the client. + */ + tosUri: string; + + /** + * OAuth 2.0 Client Creation Date + * CreatedAt returns the timestamp of the client's creation. + */ + createdAt: string; + + /** + * OAuth 2.0 Client Last Update Date + * UpdatedAt returns the timestamp of the last update. + */ + updatedAt: string; + + /** + * Metadata - JSON object + * JSONRawMessage represents a json.RawMessage that works well with JSON, SQL, and Swagger. + */ + metadata: Record = {}; + + constructor({ + clientId, + clientSecret, + clientName, + scope, + redirectUris = null, + authorizationCodeGrantAccessTokenLifespan = null, + authorizationCodeGrantIdTokenLifespan = null, + authorizationCodeGrantRefreshTokenLifespan = null, + clientCredentialsGrantAccessTokenLifespan = null, + implicitGrantAccessTokenLifespan = null, + implicitGrantIdTokenLifespan = null, + refreshTokenGrantAccessTokenLifespan = null, + refreshTokenGrantIdTokenLifespan = null, + refreshTokenGrantRefreshTokenLifespan = null, + tokenEndpointAuthMethod, + clientUri = "", + allowedCorsOrigins = [], + audience = [], + grantTypes = null, + responseTypes = null, + logoUri = "", + policyUri = "", + tosUri = "", + createdAt, + updatedAt, + metadata = {}, + }: OAuth2ClientOptions) { + this.clientId = clientId; + this.clientSecret = clientSecret; + this.clientName = clientName; + this.scope = scope; + this.redirectUris = redirectUris; + this.authorizationCodeGrantAccessTokenLifespan = authorizationCodeGrantAccessTokenLifespan; + this.authorizationCodeGrantIdTokenLifespan = authorizationCodeGrantIdTokenLifespan; + this.authorizationCodeGrantRefreshTokenLifespan = authorizationCodeGrantRefreshTokenLifespan; + this.clientCredentialsGrantAccessTokenLifespan = clientCredentialsGrantAccessTokenLifespan; + this.implicitGrantAccessTokenLifespan = implicitGrantAccessTokenLifespan; + this.implicitGrantIdTokenLifespan = implicitGrantIdTokenLifespan; + this.refreshTokenGrantAccessTokenLifespan = refreshTokenGrantAccessTokenLifespan; + this.refreshTokenGrantIdTokenLifespan = refreshTokenGrantIdTokenLifespan; + this.refreshTokenGrantRefreshTokenLifespan = refreshTokenGrantRefreshTokenLifespan; + this.tokenEndpointAuthMethod = tokenEndpointAuthMethod; + this.clientUri = clientUri; + this.allowedCorsOrigins = allowedCorsOrigins; + this.audience = audience; + this.grantTypes = grantTypes; + this.responseTypes = responseTypes; + this.logoUri = logoUri; + this.policyUri = policyUri; + this.tosUri = tosUri; + this.createdAt = createdAt; + this.updatedAt = updatedAt; + this.metadata = metadata; + } + + static fromAPIResponse(response: any): OAuth2Client { + return new OAuth2Client(transformObjectKeys(response, "camelCase")); + } +} diff --git a/lib/ts/recipe/oauth2provider/api/auth.ts b/lib/ts/recipe/oauth2provider/api/auth.ts new file mode 100644 index 000000000..c6fc92e10 --- /dev/null +++ b/lib/ts/recipe/oauth2provider/api/auth.ts @@ -0,0 +1,79 @@ +/* Copyright (c) 2024, 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 { send200Response } from "../../../utils"; +import { APIInterface, APIOptions } from ".."; +import { UserContext } from "../../../types"; +import setCookieParser from "set-cookie-parser"; +import Session from "../../session"; +import SessionError from "../../../recipe/session/error"; + +export default async function authGET( + apiImplementation: APIInterface, + options: APIOptions, + userContext: UserContext +): Promise { + if (apiImplementation.authGET === undefined) { + return false; + } + const origURL = options.req.getOriginalURL(); + const splitURL = origURL.split("?"); + const params = new URLSearchParams(splitURL[1]); + let session, shouldTryRefresh; + try { + session = await Session.getSession(options.req, options.res, { sessionRequired: false }, userContext); + shouldTryRefresh = false; + } catch (error) { + session = undefined; + if (SessionError.isErrorFromSuperTokens(error) && error.type === SessionError.TRY_REFRESH_TOKEN) { + shouldTryRefresh = true; + } else { + // This should generally not happen, but we can handle this as if the session is not present, + // because then we redirect to the frontend, which should handle the validation error + shouldTryRefresh = false; + } + } + + let response = await apiImplementation.authGET({ + options, + params: Object.fromEntries(params.entries()), + cookie: options.req.getHeaderValue("cookie"), + session, + shouldTryRefresh, + userContext, + }); + if ("redirectTo" in response) { + if (response.setCookie) { + const cookieStr = setCookieParser.splitCookiesString(response.setCookie); + const cookies = setCookieParser.parse(cookieStr); + for (const cookie of cookies) { + options.res.setCookie( + cookie.name, + cookie.value, + cookie.domain, + !!cookie.secure, + !!cookie.httpOnly, + new Date(cookie.expires!).getTime(), + cookie.path || "/", + cookie.sameSite as any + ); + } + } + options.res.original.redirect(response.redirectTo); + } else { + send200Response(options.res, response); + } + return true; +} diff --git a/lib/ts/recipe/oauth2provider/api/implementation.ts b/lib/ts/recipe/oauth2provider/api/implementation.ts new file mode 100644 index 000000000..6745cec18 --- /dev/null +++ b/lib/ts/recipe/oauth2provider/api/implementation.ts @@ -0,0 +1,121 @@ +/* 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 { APIInterface } from "../types"; +import { handleInternalRedirects, loginGET } from "./utils"; + +export default function getAPIImplementation(): APIInterface { + return { + loginGET: async ({ loginChallenge, options, session, shouldTryRefresh, userContext }) => { + const response = await loginGET({ + recipeImplementation: options.recipeImplementation, + loginChallenge, + session, + shouldTryRefresh, + isDirectCall: true, + userContext, + }); + return handleInternalRedirects({ + response, + cookie: options.req.getHeaderValue("cookie"), + recipeImplementation: options.recipeImplementation, + session, + shouldTryRefresh, + userContext, + }); + }, + + authGET: async ({ options, params, cookie, session, shouldTryRefresh, userContext }) => { + const response = await options.recipeImplementation.authorization({ + params, + cookies: cookie, + session, + userContext, + }); + + if (response.status === "OK") { + return handleInternalRedirects({ + response, + recipeImplementation: options.recipeImplementation, + cookie, + session, + shouldTryRefresh, + userContext, + }); + } + return response; + }, + tokenPOST: async (input) => { + return input.options.recipeImplementation.tokenExchange({ + authorizationHeader: input.authorizationHeader, + body: input.body, + userContext: input.userContext, + }); + }, + loginInfoGET: async ({ loginChallenge, options, userContext }) => { + const { client } = await options.recipeImplementation.getLoginRequest({ + challenge: loginChallenge, + userContext, + }); + + return { + status: "OK", + info: { + clientId: client.clientId, + clientName: client.clientName, + tosUri: client.tosUri, + policyUri: client.policyUri, + logoUri: client.logoUri, + clientUri: client.clientUri, + metadata: client.metadata, + }, + }; + }, + userInfoGET: async ({ accessTokenPayload, user, scopes, tenantId, options, userContext }) => { + return options.recipeImplementation.buildUserInfo({ + user, + accessTokenPayload, + scopes, + tenantId, + userContext, + }); + }, + revokeTokenPOST: async (input) => { + if ("authorizationHeader" in input && input.authorizationHeader !== undefined) { + return input.options.recipeImplementation.revokeToken({ + token: input.token, + authorizationHeader: input.authorizationHeader, + userContext: input.userContext, + }); + } else if ("clientId" in input && input.clientId !== undefined) { + return input.options.recipeImplementation.revokeToken({ + token: input.token, + clientId: input.clientId, + clientSecret: input.clientSecret, + userContext: input.userContext, + }); + } else { + throw new Error(`Either of 'authorizationHeader' or 'clientId' must be provided`); + } + }, + introspectTokenPOST: async (input) => { + return input.options.recipeImplementation.introspectToken({ + token: input.token, + scopes: input.scopes, + userContext: input.userContext, + }); + }, + }; +} diff --git a/lib/ts/recipe/oauth2provider/api/introspectToken.ts b/lib/ts/recipe/oauth2provider/api/introspectToken.ts new file mode 100644 index 000000000..47b4be69c --- /dev/null +++ b/lib/ts/recipe/oauth2provider/api/introspectToken.ts @@ -0,0 +1,47 @@ +/* Copyright (c) 2024, 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 { send200Response, sendNon200ResponseWithMessage } from "../../../utils"; +import { APIInterface, APIOptions } from ".."; +import { UserContext } from "../../../types"; + +export default async function introspectTokenPOST( + apiImplementation: APIInterface, + options: APIOptions, + userContext: UserContext +): Promise { + if (apiImplementation.introspectTokenPOST === undefined) { + return false; + } + + const body = await options.req.getBodyAsJSONOrFormData(); + + if (body.token === undefined) { + sendNon200ResponseWithMessage(options.res, "token is required in the request body", 400); + return true; + } + + const scopes: string[] = body.scope ? body.scope.split(" ") : []; + + let response = await apiImplementation.introspectTokenPOST({ + options, + token: body.token, + scopes, + userContext, + }); + + send200Response(options.res, response); + return true; +} diff --git a/lib/ts/recipe/oauth2provider/api/login.ts b/lib/ts/recipe/oauth2provider/api/login.ts new file mode 100644 index 000000000..9284c1e29 --- /dev/null +++ b/lib/ts/recipe/oauth2provider/api/login.ts @@ -0,0 +1,85 @@ +/* Copyright (c) 2024, 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 setCookieParser from "set-cookie-parser"; +import { send200Response } from "../../../utils"; +import { APIInterface, APIOptions } from ".."; +import Session from "../../session"; +import { UserContext } from "../../../types"; +import SuperTokensError from "../../../error"; +import SessionError from "../../../recipe/session/error"; + +export default async function login( + apiImplementation: APIInterface, + options: APIOptions, + userContext: UserContext +): Promise { + if (apiImplementation.loginGET === undefined) { + return false; + } + + let session, shouldTryRefresh; + try { + session = await Session.getSession(options.req, options.res, { sessionRequired: false }, userContext); + shouldTryRefresh = false; + } catch (error) { + // We can handle this as if the session is not present, because then we redirect to the frontend, + // which should handle the validation error + session = undefined; + if (SuperTokensError.isErrorFromSuperTokens(error) && error.type === SessionError.TRY_REFRESH_TOKEN) { + shouldTryRefresh = true; + } else { + shouldTryRefresh = false; + } + } + + const loginChallenge = + options.req.getKeyValueFromQuery("login_challenge") ?? options.req.getKeyValueFromQuery("loginChallenge"); + if (loginChallenge === undefined) { + throw new SuperTokensError({ + type: SuperTokensError.BAD_INPUT_ERROR, + message: "Missing input param: loginChallenge", + }); + } + let response = await apiImplementation.loginGET({ + options, + loginChallenge, + session, + shouldTryRefresh, + userContext, + }); + if ("status" in response) { + send200Response(options.res, response); + } else { + if (response.setCookie) { + const cookieStr = setCookieParser.splitCookiesString(response.setCookie); + const cookies = setCookieParser.parse(cookieStr); + for (const cookie of cookies) { + options.res.setCookie( + cookie.name, + cookie.value, + cookie.domain, + !!cookie.secure, + !!cookie.httpOnly, + new Date(cookie.expires!).getTime(), + cookie.path || "/", + cookie.sameSite as any + ); + } + } + options.res.original.redirect(response.redirectTo); + } + return true; +} diff --git a/lib/ts/recipe/oauth2provider/api/loginInfo.ts b/lib/ts/recipe/oauth2provider/api/loginInfo.ts new file mode 100644 index 000000000..2c13ddad0 --- /dev/null +++ b/lib/ts/recipe/oauth2provider/api/loginInfo.ts @@ -0,0 +1,48 @@ +/* Copyright (c) 2024, 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 { send200Response } from "../../../utils"; +import { APIInterface, APIOptions } from ".."; +import { UserContext } from "../../../types"; +import SuperTokensError from "../../../error"; + +export default async function loginInfoGET( + apiImplementation: APIInterface, + options: APIOptions, + userContext: UserContext +): Promise { + if (apiImplementation.loginInfoGET === undefined) { + return false; + } + + const loginChallenge = + options.req.getKeyValueFromQuery("login_challenge") ?? options.req.getKeyValueFromQuery("loginChallenge"); + + if (loginChallenge === undefined) { + throw new SuperTokensError({ + type: SuperTokensError.BAD_INPUT_ERROR, + message: "Missing input param: loginChallenge", + }); + } + + let response = await apiImplementation.loginInfoGET({ + options, + loginChallenge, + userContext, + }); + + send200Response(options.res, response); + return true; +} diff --git a/lib/ts/recipe/oauth2provider/api/revokeToken.ts b/lib/ts/recipe/oauth2provider/api/revokeToken.ts new file mode 100644 index 000000000..9974f7795 --- /dev/null +++ b/lib/ts/recipe/oauth2provider/api/revokeToken.ts @@ -0,0 +1,62 @@ +/* Copyright (c) 2024, 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 { send200Response, sendNon200Response, sendNon200ResponseWithMessage } from "../../../utils"; +import { APIInterface, APIOptions } from ".."; +import { UserContext } from "../../../types"; + +export default async function revokeTokenPOST( + apiImplementation: APIInterface, + options: APIOptions, + userContext: UserContext +): Promise { + if (apiImplementation.revokeTokenPOST === undefined) { + return false; + } + + const body = await options.req.getBodyAsJSONOrFormData(); + + if (body.token === undefined) { + sendNon200ResponseWithMessage(options.res, "token is required in the request body", 400); + return true; + } + + const authorizationHeader = options.req.getHeaderValue("authorization"); + + if (authorizationHeader !== undefined && (body.client_id !== undefined || body.client_secret !== undefined)) { + sendNon200ResponseWithMessage( + options.res, + "Only one of authorization header or client_id and client_secret can be provided", + 400 + ); + return true; + } + + let response = await apiImplementation.revokeTokenPOST({ + options, + authorizationHeader, + token: body.token, + clientId: body.client_id, + clientSecret: body.client_secret, + userContext, + }); + + if ("statusCode" in response && response.statusCode !== 200) { + sendNon200Response(options.res, response.statusCode!, response); + } else { + send200Response(options.res, response); + } + return true; +} diff --git a/lib/ts/recipe/oauth2provider/api/token.ts b/lib/ts/recipe/oauth2provider/api/token.ts new file mode 100644 index 000000000..0ed290282 --- /dev/null +++ b/lib/ts/recipe/oauth2provider/api/token.ts @@ -0,0 +1,48 @@ +/* Copyright (c) 2024, 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 { send200Response, sendNon200Response } from "../../../utils"; +import { APIInterface, APIOptions } from ".."; +import { UserContext } from "../../../types"; + +export default async function tokenPOST( + apiImplementation: APIInterface, + options: APIOptions, + userContext: UserContext +): Promise { + if (apiImplementation.tokenPOST === undefined) { + return false; + } + + const authorizationHeader = options.req.getHeaderValue("authorization"); + + let response = await apiImplementation.tokenPOST({ + authorizationHeader, + options, + body: await options.req.getBodyAsJSONOrFormData(), + userContext, + }); + + if ("statusCode" in response && response.statusCode !== 200) { + sendNon200Response(options.res, response.statusCode!, { + error: response.error, + error_description: response.errorDescription, + }); + } else { + send200Response(options.res, response); + } + + return true; +} diff --git a/lib/ts/recipe/oauth2provider/api/userInfo.ts b/lib/ts/recipe/oauth2provider/api/userInfo.ts new file mode 100644 index 000000000..3e661068d --- /dev/null +++ b/lib/ts/recipe/oauth2provider/api/userInfo.ts @@ -0,0 +1,92 @@ +/* Copyright (c) 2024, 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 OAuth2ProviderRecipe from "../recipe"; +import { send200Response, sendNon200ResponseWithMessage } from "../../../utils"; +import { APIInterface, APIOptions } from ".."; +import { JSONObject, UserContext } from "../../../types"; +import { getUser } from "../../.."; + +export default async function userInfoGET( + apiImplementation: APIInterface, + tenantId: string, + options: APIOptions, + userContext: UserContext +): Promise { + if (apiImplementation.userInfoGET === undefined) { + return false; + } + + const authHeader = options.req.getHeaderValue("authorization"); + + if (authHeader === undefined || !authHeader.startsWith("Bearer ")) { + sendNon200ResponseWithMessage(options.res, "Missing or invalid Authorization header", 401); + return true; + } + + const accessToken = authHeader.replace(/^Bearer /, "").trim(); + + let accessTokenPayload: JSONObject; + + try { + const { + payload, + } = await OAuth2ProviderRecipe.getInstanceOrThrowError().recipeInterfaceImpl.validateOAuth2AccessToken({ + token: accessToken, + userContext, + }); + accessTokenPayload = payload; + } catch (error) { + options.res.setHeader("WWW-Authenticate", 'Bearer error="invalid_token"', false); + options.res.setHeader("Access-Control-Expose-Headers", "WWW-Authenticate", true); + sendNon200ResponseWithMessage(options.res, "Invalid or expired OAuth2 access token", 401); + return true; + } + + if ( + accessTokenPayload === null || + typeof accessTokenPayload !== "object" || + typeof accessTokenPayload.sub !== "string" || + !Array.isArray(accessTokenPayload.scp) + ) { + options.res.setHeader("WWW-Authenticate", 'Bearer error="invalid_token"', false); + options.res.setHeader("Access-Control-Expose-Headers", "WWW-Authenticate", true); + sendNon200ResponseWithMessage(options.res, "Malformed access token payload", 401); + return true; + } + + const userId = accessTokenPayload.sub; + + const user = await getUser(userId, userContext); + + if (user === undefined) { + options.res.setHeader("WWW-Authenticate", 'Bearer error="invalid_token"', false); + options.res.setHeader("Access-Control-Expose-Headers", "WWW-Authenticate", true); + sendNon200ResponseWithMessage(options.res, "Couldn't find any user associated with the access token", 401); + return true; + } + + const response = await apiImplementation.userInfoGET({ + accessTokenPayload, + user, + tenantId, + scopes: accessTokenPayload.scp as string[], + options, + userContext, + }); + + send200Response(options.res, response); + return true; +} diff --git a/lib/ts/recipe/oauth2provider/api/utils.ts b/lib/ts/recipe/oauth2provider/api/utils.ts new file mode 100644 index 000000000..c062a7fb7 --- /dev/null +++ b/lib/ts/recipe/oauth2provider/api/utils.ts @@ -0,0 +1,272 @@ +import SuperTokens from "../../../supertokens"; +import { UserContext } from "../../../types"; +import { DEFAULT_TENANT_ID } from "../../multitenancy/constants"; +import { getSessionInformation } from "../../session"; +import { SessionContainerInterface } from "../../session/types"; +import { AUTH_PATH, LOGIN_PATH } from "../constants"; +import { RecipeInterface } from "../types"; +import setCookieParser from "set-cookie-parser"; + +// API implementation for the loginGET function. +// Extracted for use in both apiImplementation and handleInternalRedirects. +export async function loginGET({ + recipeImplementation, + loginChallenge, + shouldTryRefresh, + session, + setCookie, + isDirectCall, + userContext, +}: { + recipeImplementation: RecipeInterface; + loginChallenge: string; + session?: SessionContainerInterface; + shouldTryRefresh: boolean; + setCookie?: string; + userContext: UserContext; + isDirectCall: boolean; +}) { + const loginRequest = await recipeImplementation.getLoginRequest({ + challenge: loginChallenge, + userContext, + }); + + const sessionInfo = session !== undefined ? await getSessionInformation(session?.getHandle()) : undefined; + if (!sessionInfo) { + session = undefined; + } + + const incomingAuthUrlQueryParams = new URLSearchParams(loginRequest.requestUrl.split("?")[1]); + const promptParam = incomingAuthUrlQueryParams.get("prompt") ?? incomingAuthUrlQueryParams.get("st_prompt"); + const maxAgeParam = incomingAuthUrlQueryParams.get("max_age"); + if (maxAgeParam !== null) { + try { + const maxAgeParsed = Number.parseInt(maxAgeParam); + if (maxAgeParsed < 0) { + const reject = await recipeImplementation.rejectLoginRequest({ + challenge: loginChallenge, + error: { + status: "OAUTH_ERROR", + error: "invalid_request", + errorDescription: "max_age cannot be negative", + }, + userContext, + }); + return { redirectTo: reject.redirectTo, setCookie }; + } + } catch { + const reject = await recipeImplementation.rejectLoginRequest({ + challenge: loginChallenge, + error: { + status: "OAUTH_ERROR", + error: "invalid_request", + errorDescription: "max_age must be an integer", + }, + userContext, + }); + return { redirectTo: reject.redirectTo, setCookie }; + } + } + const tenantIdParam = incomingAuthUrlQueryParams.get("tenant_id"); + if ( + session && + (["", undefined].includes(loginRequest.subject) || session.getUserId() === loginRequest.subject) && + (["", null].includes(tenantIdParam) || session.getTenantId() === tenantIdParam) && + (promptParam !== "login" || isDirectCall) && + (maxAgeParam === null || + (maxAgeParam === "0" && isDirectCall) || + Number.parseInt(maxAgeParam) * 1000 > Date.now() - sessionInfo!.timeCreated) + ) { + const accept = await recipeImplementation.acceptLoginRequest({ + challenge: loginChallenge, + subject: session.getUserId(), + identityProviderSessionId: session.getHandle(), + remember: true, + rememberFor: 3600, + userContext, + }); + return { redirectTo: accept.redirectTo, setCookie }; + } + const appInfo = SuperTokens.getInstanceOrThrowError().appInfo; + const websiteDomain = appInfo + .getOrigin({ + request: undefined, + userContext: userContext, + }) + .getAsStringDangerous(); + const websiteBasePath = appInfo.websiteBasePath.getAsStringDangerous(); + if (shouldTryRefresh) { + const websiteDomain = appInfo + .getOrigin({ + request: undefined, + userContext: userContext, + }) + .getAsStringDangerous(); + const websiteBasePath = appInfo.websiteBasePath.getAsStringDangerous(); + + const queryParamsForTryRefreshPage = new URLSearchParams({ + loginChallenge, + }); + + return { + redirectTo: websiteDomain + websiteBasePath + `/try-refresh?${queryParamsForTryRefreshPage.toString()}`, + setCookie, + }; + } + if (promptParam === "none") { + const reject = await recipeImplementation.rejectLoginRequest({ + challenge: loginChallenge, + error: { + status: "OAUTH_ERROR", + error: "login_required", + errorDescription: + "The Authorization Server requires End-User authentication. Prompt 'none' was requested, but no existing or expired login session was found.", + }, + userContext, + }); + return { redirectTo: reject.redirectTo, setCookie }; + } + + const queryParamsForAuthPage = new URLSearchParams({ + loginChallenge, + }); + + if (loginRequest.oidcContext?.login_hint) { + queryParamsForAuthPage.set("hint", loginRequest.oidcContext.login_hint); + } + + if (session !== undefined || promptParam === "login") { + queryParamsForAuthPage.set("forceFreshAuth", "true"); + } + + if (tenantIdParam !== null && tenantIdParam !== DEFAULT_TENANT_ID) { + queryParamsForAuthPage.set("tenantId", tenantIdParam); + } + + return { + redirectTo: websiteDomain + websiteBasePath + `?${queryParamsForAuthPage.toString()}`, + setCookie, + }; +} + +function getMergedCookies({ cookie = "", setCookie }: { cookie?: string; setCookie?: string }): string { + if (!setCookie) { + return cookie; + } + + const cookieMap = cookie.split(";").reduce((acc, curr) => { + const [name, value] = curr.split("="); + return { ...acc, [name.trim()]: value }; + }, {} as Record); + + const setCookies = setCookieParser.parse(setCookieParser.splitCookiesString(setCookie)); + + for (const { name, value, expires } of setCookies) { + if (expires && new Date(expires) < new Date()) { + delete cookieMap[name]; + } else { + cookieMap[name] = value; + } + } + + return Object.entries(cookieMap) + .map(([key, value]) => `${key}=${value}`) + .join(";"); +} + +function mergeSetCookieHeaders(setCookie1?: string, setCookie2?: string): string { + if (!setCookie1) { + return setCookie2 || ""; + } + if (!setCookie2 || setCookie1 === setCookie2) { + return setCookie1; + } + return `${setCookie1}, ${setCookie2}`; +} + +function isInternalRedirect(redirectTo: string): boolean { + const { apiDomain, apiBasePath } = SuperTokens.getInstanceOrThrowError().appInfo; + const basePath = `${apiDomain.getAsStringDangerous()}${apiBasePath.getAsStringDangerous()}`; + return [ + LOGIN_PATH, + AUTH_PATH, + LOGIN_PATH.replace("oauth", "oauth2"), + AUTH_PATH.replace("oauth", "oauth2"), + ].some((path) => redirectTo.startsWith(`${basePath}${path}`)); +} + +// In the OAuth2 flow, we do several internal redirects. These redirects don't require a frontend-to-api-server round trip. +// If an internal redirect is identified, it's handled directly by this function. +// Currently, we only need to handle redirects to /oauth/login and /oauth/auth endpoints. +export async function handleInternalRedirects({ + response, + recipeImplementation, + session, + shouldTryRefresh, + cookie = "", + userContext, +}: { + response: { redirectTo: string; setCookie: string | undefined }; + recipeImplementation: RecipeInterface; + session?: SessionContainerInterface; + shouldTryRefresh: boolean; + cookie?: string; + userContext: UserContext; +}): Promise<{ redirectTo: string; setCookie: string | undefined }> { + if (!isInternalRedirect(response.redirectTo)) { + return response; + } + + // Typically, there are no more than 2 internal redirects per API call but we are allowing upto 10. + // This safety net prevents infinite redirect loops in case there are more redirects than expected. + const maxRedirects = 10; + let redirectCount = 0; + + while (redirectCount < maxRedirects && isInternalRedirect(response.redirectTo)) { + cookie = getMergedCookies({ cookie, setCookie: response.setCookie }); + + const queryString = response.redirectTo.split("?")[1]; + const params = new URLSearchParams(queryString); + + if (response.redirectTo.includes(LOGIN_PATH)) { + const loginChallenge = params.get("login_challenge") ?? params.get("loginChallenge"); + if (!loginChallenge) { + throw new Error(`Expected loginChallenge in ${response.redirectTo}`); + } + + const loginRes = await loginGET({ + recipeImplementation, + loginChallenge, + session, + shouldTryRefresh, + setCookie: response.setCookie, + isDirectCall: false, + userContext, + }); + + response = { + redirectTo: loginRes.redirectTo, + setCookie: mergeSetCookieHeaders(loginRes.setCookie, response.setCookie), + }; + } else if (response.redirectTo.includes(AUTH_PATH)) { + const authRes = await recipeImplementation.authorization({ + params: Object.fromEntries(params.entries()), + cookies: cookie, + session, + userContext, + }); + + if (authRes.status === "OK") { + response = { + redirectTo: authRes.redirectTo, + setCookie: mergeSetCookieHeaders(authRes.setCookie, response.setCookie), + }; + } + } else { + throw new Error(`Unexpected internal redirect ${response.redirectTo}`); + } + + redirectCount++; + } + return response; +} diff --git a/lib/ts/recipe/oauth2provider/constants.ts b/lib/ts/recipe/oauth2provider/constants.ts new file mode 100644 index 000000000..19c7173e3 --- /dev/null +++ b/lib/ts/recipe/oauth2provider/constants.ts @@ -0,0 +1,24 @@ +/* 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. + */ + +export const OAUTH2_BASE_PATH = "/oauth/"; + +export const LOGIN_PATH = "/oauth/login"; +export const AUTH_PATH = "/oauth/auth"; +export const TOKEN_PATH = "/oauth/token"; +export const LOGIN_INFO_PATH = "/oauth/login/info"; +export const USER_INFO_PATH = "/oauth/userinfo"; +export const REVOKE_TOKEN_PATH = "/oauth/revoke"; +export const INTROSPECT_TOKEN_PATH = "/oauth/introspect"; diff --git a/lib/ts/recipe/oauth2provider/index.ts b/lib/ts/recipe/oauth2provider/index.ts new file mode 100644 index 000000000..01424b051 --- /dev/null +++ b/lib/ts/recipe/oauth2provider/index.ts @@ -0,0 +1,165 @@ +/* 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 { getUserContext } from "../../utils"; +import Recipe from "./recipe"; +import { + APIInterface, + RecipeInterface, + APIOptions, + CreateOAuth2ClientInput, + UpdateOAuth2ClientInput, + DeleteOAuth2ClientInput, + GetOAuth2ClientsInput, +} from "./types"; + +export default class Wrapper { + static init = Recipe.init; + + static async getOAuth2Client(clientId: string, userContext?: Record) { + return await Recipe.getInstanceOrThrowError().recipeInterfaceImpl.getOAuth2Client({ + clientId, + userContext: getUserContext(userContext), + }); + } + static async getOAuth2Clients(input: GetOAuth2ClientsInput, userContext?: Record) { + return await Recipe.getInstanceOrThrowError().recipeInterfaceImpl.getOAuth2Clients({ + ...input, + userContext: getUserContext(userContext), + }); + } + static async createOAuth2Client(input: CreateOAuth2ClientInput, userContext?: Record) { + return await Recipe.getInstanceOrThrowError().recipeInterfaceImpl.createOAuth2Client({ + ...input, + userContext: getUserContext(userContext), + }); + } + static async updateOAuth2Client(input: UpdateOAuth2ClientInput, userContext?: Record) { + return await Recipe.getInstanceOrThrowError().recipeInterfaceImpl.updateOAuth2Client({ + ...input, + userContext: getUserContext(userContext), + }); + } + static async deleteOAuth2Client(input: DeleteOAuth2ClientInput, userContext?: Record) { + return await Recipe.getInstanceOrThrowError().recipeInterfaceImpl.deleteOAuth2Client({ + ...input, + userContext: getUserContext(userContext), + }); + } + + static validateOAuth2AccessToken( + token: string, + requirements?: { + clientId?: string; + scopes?: string[]; + audience?: string; + }, + checkDatabase?: boolean, + userContext?: Record + ) { + return Recipe.getInstanceOrThrowError().recipeInterfaceImpl.validateOAuth2AccessToken({ + token, + requirements, + checkDatabase, + userContext: getUserContext(userContext), + }); + } + + static createTokenForClientCredentials( + clientId: string, + clientSecret: string, + scope?: string[], + audience?: string, + userContext?: Record + ) { + return Recipe.getInstanceOrThrowError().recipeInterfaceImpl.tokenExchange({ + body: { + grant_type: "client_credentials", + client_id: clientId, + client_secret: clientSecret, + scope: scope?.join(" "), + audience: audience, + }, + userContext: getUserContext(userContext), + }); + } + + static async revokeToken( + token: string, + clientId: string, + clientSecret?: string, + userContext?: Record + ) { + let authorizationHeader: string | undefined = undefined; + + const normalisedUserContext = getUserContext(userContext); + const recipeInterfaceImpl = Recipe.getInstanceOrThrowError().recipeInterfaceImpl; + + const res = await recipeInterfaceImpl.getOAuth2Client({ clientId, userContext: normalisedUserContext }); + + if (res.status !== "OK") { + throw new Error(`Failed to get OAuth2 client with id ${clientId}: ${res.error}`); + } + + const { tokenEndpointAuthMethod } = res.client; + + if (tokenEndpointAuthMethod === "none") { + authorizationHeader = "Basic " + Buffer.from(clientId + ":").toString("base64"); + } else if (tokenEndpointAuthMethod === "client_secret_basic") { + authorizationHeader = "Basic " + Buffer.from(clientId + ":" + clientSecret).toString("base64"); + } + + if (authorizationHeader !== undefined) { + return await recipeInterfaceImpl.revokeToken({ + token, + authorizationHeader, + userContext: normalisedUserContext, + }); + } + + return await recipeInterfaceImpl.revokeToken({ + token, + clientId, + clientSecret, + userContext: normalisedUserContext, + }); + } + + static validateOAuth2RefreshToken(token: string, scopes?: string[], userContext?: Record) { + return Recipe.getInstanceOrThrowError().recipeInterfaceImpl.introspectToken({ + token, + scopes, + userContext: getUserContext(userContext), + }); + } +} + +export let init = Wrapper.init; + +export let getOAuth2Clients = Wrapper.getOAuth2Clients; + +export let createOAuth2Client = Wrapper.createOAuth2Client; + +export let updateOAuth2Client = Wrapper.updateOAuth2Client; + +export let deleteOAuth2Client = Wrapper.deleteOAuth2Client; + +export let validateOAuth2AccessToken = Wrapper.validateOAuth2AccessToken; + +export let createTokenForClientCredentials = Wrapper.createTokenForClientCredentials; + +export let revokeToken = Wrapper.revokeToken; + +export type { APIInterface, APIOptions, RecipeInterface }; diff --git a/lib/ts/recipe/oauth2provider/recipe.ts b/lib/ts/recipe/oauth2provider/recipe.ts new file mode 100644 index 000000000..861d40f68 --- /dev/null +++ b/lib/ts/recipe/oauth2provider/recipe.ts @@ -0,0 +1,341 @@ +/* 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 SuperTokensError from "../../error"; +import error from "../../error"; +import type { BaseRequest, BaseResponse } from "../../framework"; +import NormalisedURLPath from "../../normalisedURLPath"; +import { Querier } from "../../querier"; +import RecipeModule from "../../recipeModule"; +import { APIHandled, HTTPMethod, JSONObject, NormalisedAppinfo, RecipeListFunction, UserContext } from "../../types"; +import authGET from "./api/auth"; +import APIImplementation from "./api/implementation"; +import loginAPI from "./api/login"; +import tokenPOST from "./api/token"; +import loginInfoGET from "./api/loginInfo"; +import { + AUTH_PATH, + INTROSPECT_TOKEN_PATH, + LOGIN_INFO_PATH, + LOGIN_PATH, + REVOKE_TOKEN_PATH, + TOKEN_PATH, + USER_INFO_PATH, +} from "./constants"; +import RecipeImplementation from "./recipeImplementation"; +import { + APIInterface, + PayloadBuilderFunction, + RecipeInterface, + TypeInput, + TypeNormalisedInput, + UserInfo, + UserInfoBuilderFunction, +} from "./types"; +import { validateAndNormaliseUserInput } from "./utils"; +import OverrideableBuilder from "supertokens-js-override"; +import { User } from "../../user"; +import userInfoGET from "./api/userInfo"; +import { resetCombinedJWKS } from "../../combinedRemoteJWKSet"; +import revokeTokenPOST from "./api/revokeToken"; +import introspectTokenPOST from "./api/introspectToken"; +import { getSessionInformation } from "../session"; +import { send200Response } from "../../utils"; + +const tokenHookMap = new Map(); + +export default class Recipe extends RecipeModule { + static RECIPE_ID = "oauth2provider"; + private static instance: Recipe | undefined = undefined; + private accessTokenBuilders: PayloadBuilderFunction[] = []; + private idTokenBuilders: PayloadBuilderFunction[] = []; + private userInfoBuilders: UserInfoBuilderFunction[] = []; + + config: TypeNormalisedInput; + recipeInterfaceImpl: RecipeInterface; + apiImpl: APIInterface; + isInServerlessEnv: boolean; + + constructor(recipeId: string, appInfo: NormalisedAppinfo, isInServerlessEnv: boolean, config?: TypeInput) { + super(recipeId, appInfo); + this.config = validateAndNormaliseUserInput(this, appInfo, config); + this.isInServerlessEnv = isInServerlessEnv; + + { + let builder = new OverrideableBuilder( + RecipeImplementation( + Querier.getNewInstanceOrThrowError(recipeId), + this.config, + appInfo, + this.getDefaultAccessTokenPayload.bind(this), + this.getDefaultIdTokenPayload.bind(this), + this.getDefaultUserInfoPayload.bind(this), + this.saveTokensForHook.bind(this) + ) + ); + this.recipeInterfaceImpl = builder.override(this.config.override.functions).build(); + } + { + let builder = new OverrideableBuilder(APIImplementation()); + this.apiImpl = builder.override(this.config.override.apis).build(); + } + } + + /* Init functions */ + + static getInstance(): Recipe | undefined { + return Recipe.instance; + } + static getInstanceOrThrowError(): Recipe { + if (Recipe.instance !== undefined) { + return Recipe.instance; + } + throw new Error("Initialisation not done. Did you forget to call the Jwt.init function?"); + } + + static init(config?: TypeInput): RecipeListFunction { + return (appInfo, isInServerlessEnv) => { + if (Recipe.instance === undefined) { + Recipe.instance = new Recipe(Recipe.RECIPE_ID, appInfo, isInServerlessEnv, config); + return Recipe.instance; + } else { + throw new Error("OAuth2Provider recipe has already been initialised. Please check your code for bugs."); + } + }; + } + + static reset() { + if (process.env.TEST_MODE !== "testing") { + throw new Error("calling testing function in non testing env"); + } + resetCombinedJWKS(); + Recipe.instance = undefined; + } + + addUserInfoBuilderFromOtherRecipe = (userInfoBuilderFn: UserInfoBuilderFunction) => { + this.userInfoBuilders.push(userInfoBuilderFn); + }; + addAccessTokenBuilderFromOtherRecipe = (accessTokenBuilders: PayloadBuilderFunction) => { + this.accessTokenBuilders.push(accessTokenBuilders); + }; + addIdTokenBuilderFromOtherRecipe = (idTokenBuilder: PayloadBuilderFunction) => { + this.idTokenBuilders.push(idTokenBuilder); + }; + saveTokensForHook = (sessionHandle: string, idToken: JSONObject, accessToken: JSONObject) => { + tokenHookMap.set(sessionHandle, { idToken, accessToken }); + }; + + /* RecipeModule functions */ + + getAPIsHandled(): APIHandled[] { + return [ + { + method: "get", + pathWithoutApiBasePath: new NormalisedURLPath(LOGIN_PATH), + id: LOGIN_PATH, + disabled: this.apiImpl.loginGET === undefined, + }, + { + method: "post", + pathWithoutApiBasePath: new NormalisedURLPath(TOKEN_PATH), + id: TOKEN_PATH, + disabled: this.apiImpl.tokenPOST === undefined, + }, + { + method: "get", + pathWithoutApiBasePath: new NormalisedURLPath(AUTH_PATH), + id: AUTH_PATH, + disabled: this.apiImpl.authGET === undefined, + }, + { + method: "get", + pathWithoutApiBasePath: new NormalisedURLPath(LOGIN_INFO_PATH), + id: LOGIN_INFO_PATH, + disabled: this.apiImpl.loginInfoGET === undefined, + }, + { + method: "get", + pathWithoutApiBasePath: new NormalisedURLPath(USER_INFO_PATH), + id: USER_INFO_PATH, + disabled: this.apiImpl.userInfoGET === undefined, + }, + { + method: "post", + pathWithoutApiBasePath: new NormalisedURLPath(REVOKE_TOKEN_PATH), + id: REVOKE_TOKEN_PATH, + disabled: this.apiImpl.revokeTokenPOST === undefined, + }, + { + method: "post", + pathWithoutApiBasePath: new NormalisedURLPath(INTROSPECT_TOKEN_PATH), + id: INTROSPECT_TOKEN_PATH, + disabled: this.apiImpl.introspectTokenPOST === undefined, + }, + { + // TODO: remove this once we get core support + method: "post", + pathWithoutApiBasePath: new NormalisedURLPath("/oauth/token-hook"), + id: "token-hook", + disabled: false, + }, + ]; + } + + handleAPIRequest = async ( + id: string, + tenantId: string, + req: BaseRequest, + res: BaseResponse, + _path: NormalisedURLPath, + _method: HTTPMethod, + userContext: UserContext + ): Promise => { + let options = { + config: this.config, + recipeId: this.getRecipeId(), + isInServerlessEnv: this.isInServerlessEnv, + recipeImplementation: this.recipeInterfaceImpl, + req, + res, + }; + + if (id === LOGIN_PATH) { + return loginAPI(this.apiImpl, options, userContext); + } + if (id === TOKEN_PATH) { + return tokenPOST(this.apiImpl, options, userContext); + } + if (id === AUTH_PATH) { + return authGET(this.apiImpl, options, userContext); + } + if (id === LOGIN_INFO_PATH) { + return loginInfoGET(this.apiImpl, options, userContext); + } + if (id === USER_INFO_PATH) { + return userInfoGET(this.apiImpl, tenantId, options, userContext); + } + if (id === REVOKE_TOKEN_PATH) { + return revokeTokenPOST(this.apiImpl, options, userContext); + } + if (id === INTROSPECT_TOKEN_PATH) { + return introspectTokenPOST(this.apiImpl, options, userContext); + } + if (id === "token-hook") { + const body = await options.req.getBodyAsJSONOrFormData(); + const sessionHandle = body.session.extra.sessionHandle; + const tokens = tokenHookMap.get(sessionHandle); + + if (tokens !== undefined) { + const { idToken, accessToken } = tokens; + send200Response(options.res, { + session: { + access_token: accessToken, + id_token: idToken, + }, + }); + } else { + send200Response(options.res, {}); + } + return true; + } + throw new Error("Should never come here: handleAPIRequest called with unknown id"); + }; + + handleError(error: error, _: BaseRequest, __: BaseResponse, _userContext: UserContext): Promise { + throw error; + } + + getAllCORSHeaders(): string[] { + return []; + } + + isErrorFromThisRecipe(err: any): err is error { + return SuperTokensError.isErrorFromSuperTokens(err) && err.fromRecipe === Recipe.RECIPE_ID; + } + + async getDefaultAccessTokenPayload(user: User, scopes: string[], sessionHandle: string, userContext: UserContext) { + const sessionInfo = await getSessionInformation(sessionHandle); + if (sessionInfo === undefined) { + throw new Error("Session not found"); + } + let payload: JSONObject = { + tId: sessionInfo.tenantId, + rsub: sessionInfo.recipeUserId.getAsString(), + sessionHandle: sessionHandle, + }; + + for (const fn of this.accessTokenBuilders) { + payload = { + ...payload, + ...(await fn(user, scopes, sessionHandle, userContext)), + }; + } + + return payload; + } + async getDefaultIdTokenPayload(user: User, scopes: string[], sessionHandle: string, userContext: UserContext) { + let payload: JSONObject = {}; + if (scopes.includes("email")) { + payload.email = user?.emails[0]; + payload.email_verified = user.loginMethods.some((lm) => lm.hasSameEmailAs(user?.emails[0]) && lm.verified); + } + if (scopes.includes("phoneNumber")) { + payload.phoneNumber = user?.phoneNumbers[0]; + payload.phoneNumber_verified = user.loginMethods.some( + (lm) => lm.hasSamePhoneNumberAs(user?.phoneNumbers[0]) && lm.verified + ); + } + + for (const fn of this.idTokenBuilders) { + payload = { + ...payload, + ...(await fn(user, scopes, sessionHandle, userContext)), + }; + } + + return payload; + } + + async getDefaultUserInfoPayload( + user: User, + accessTokenPayload: JSONObject, + scopes: string[], + tenantId: string, + userContext: UserContext + ) { + let payload: JSONObject = { + sub: accessTokenPayload.sub, + }; + if (scopes.includes("email")) { + payload.email = user?.emails[0]; + payload.email_verified = user.loginMethods.some((lm) => lm.hasSameEmailAs(user?.emails[0]) && lm.verified); + } + if (scopes.includes("phoneNumber")) { + payload.phoneNumber = user?.phoneNumbers[0]; + payload.phoneNumber_verified = user.loginMethods.some( + (lm) => lm.hasSamePhoneNumberAs(user?.phoneNumbers[0]) && lm.verified + ); + } + + for (const fn of this.userInfoBuilders) { + payload = { + ...payload, + ...(await fn(user, accessTokenPayload, scopes, tenantId, userContext)), + }; + } + + return payload as UserInfo; + } +} diff --git a/lib/ts/recipe/oauth2provider/recipeImplementation.ts b/lib/ts/recipe/oauth2provider/recipeImplementation.ts new file mode 100644 index 000000000..538377ed8 --- /dev/null +++ b/lib/ts/recipe/oauth2provider/recipeImplementation.ts @@ -0,0 +1,609 @@ +/* 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 * as jose from "jose"; +import NormalisedURLPath from "../../normalisedURLPath"; +import { Querier, hydraPubDomain } from "../../querier"; +import { JSONObject, NormalisedAppinfo } from "../../types"; +import { + RecipeInterface, + TypeNormalisedInput, + ConsentRequest, + LoginRequest, + PayloadBuilderFunction, + UserInfoBuilderFunction, +} from "./types"; +import { toSnakeCase, transformObjectKeys } from "../../utils"; +import { OAuth2Client } from "./OAuth2Client"; +import { getUser } from "../.."; +import { getCombinedJWKS } from "../../combinedRemoteJWKSet"; +import { getSessionInformation } from "../session"; + +function getUpdatedRedirectTo(appInfo: NormalisedAppinfo, redirectTo: string) { + if (redirectTo.includes("{apiDomain}")) { + return redirectTo.replace( + "{apiDomain}", + appInfo.apiDomain.getAsStringDangerous() + appInfo.apiBasePath.getAsStringDangerous() + ); + } + + // TODO: Remove this core changes are done + return redirectTo + .replace(hydraPubDomain, appInfo.apiDomain.getAsStringDangerous() + appInfo.apiBasePath.getAsStringDangerous()) + .replace("oauth2/", "oauth/"); +} + +export default function getRecipeInterface( + querier: Querier, + _config: TypeNormalisedInput, + appInfo: NormalisedAppinfo, + getDefaultAccessTokenPayload: PayloadBuilderFunction, + getDefaultIdTokenPayload: PayloadBuilderFunction, + getDefaultUserInfoPayload: UserInfoBuilderFunction, + saveTokensForHook: (sessionHandle: string, idToken: JSONObject, accessToken: JSONObject) => void +): RecipeInterface { + return { + getLoginRequest: async function (this: RecipeInterface, input): Promise { + const resp = await querier.sendGetRequest( + new NormalisedURLPath("/recipe/oauth/auth/requests/login"), + { challenge: input.challenge }, + input.userContext + ); + + return { + challenge: resp.challenge, + client: OAuth2Client.fromAPIResponse(resp.client), + oidcContext: resp.oidcContext, + requestUrl: resp.requestUrl, + requestedAccessTokenAudience: resp.requestedAccessTokenAudience, + requestedScope: resp.requestedScope, + sessionId: resp.sessionId, + skip: resp.skip, + subject: resp.subject, + }; + }, + acceptLoginRequest: async function (this: RecipeInterface, input): Promise<{ redirectTo: string }> { + const resp = await querier.sendPutRequest( + new NormalisedURLPath(`/recipe/oauth/auth/requests/login/accept`), + { + acr: input.acr, + amr: input.amr, + context: input.context, + extendSessionLifespan: input.extendSessionLifespan, + forceSubjectIdentifier: input.forceSubjectIdentifier, + identityProviderSessionId: input.identityProviderSessionId, + remember: input.remember, + rememberFor: input.rememberFor, + subject: input.subject, + }, + { + challenge: input.challenge, + }, + input.userContext + ); + + return { + redirectTo: getUpdatedRedirectTo(appInfo, resp.redirectTo), + }; + }, + rejectLoginRequest: async function (this: RecipeInterface, input): Promise<{ redirectTo: string }> { + const resp = await querier.sendPutRequest( + new NormalisedURLPath(`/recipe/oauth/auth/requests/login/reject`), + { + error: input.error.error, + error_description: input.error.errorDescription, + status_code: input.error.statusCode, + }, + { + login_challenge: input.challenge, + }, + input.userContext + ); + + return { + redirectTo: getUpdatedRedirectTo(appInfo, resp.redirectTo), + }; + }, + + getConsentRequest: async function (this: RecipeInterface, input): Promise { + const resp = await querier.sendGetRequest( + new NormalisedURLPath("/recipe/oauth/auth/requests/consent"), + { challenge: input.challenge }, + input.userContext + ); + + return { + acr: resp.acr, + amr: resp.amr, + challenge: resp.challenge, + client: OAuth2Client.fromAPIResponse(resp.client), + context: resp.context, + loginChallenge: resp.loginChallenge, + loginSessionId: resp.loginSessionId, + oidcContext: resp.oidcContext, + requestedAccessTokenAudience: resp.requestedAccessTokenAudience, + requestedScope: resp.requestedScope, + skip: resp.skip, + subject: resp.subject, + }; + }, + acceptConsentRequest: async function (this: RecipeInterface, input): Promise<{ redirectTo: string }> { + const resp = await querier.sendPutRequest( + new NormalisedURLPath(`/recipe/oauth/auth/requests/consent/accept`), + { + context: input.context, + grantAccessTokenAudience: input.grantAccessTokenAudience, + grantScope: input.grantScope, + handledAt: input.handledAt, + remember: input.remember, + rememberFor: input.rememberFor, + session: input.session, + }, + { + challenge: input.challenge, + }, + input.userContext + ); + + return { + redirectTo: getUpdatedRedirectTo(appInfo, resp.redirectTo), + }; + }, + rejectConsentRequest: async function (this: RecipeInterface, input) { + const resp = await querier.sendPutRequest( + new NormalisedURLPath(`/recipe/oauth/auth/requests/consent/reject`), + { + error: input.error.error, + error_description: input.error.errorDescription, + status_code: input.error.statusCode, + }, + { + consentChallenge: input.challenge, + }, + input.userContext + ); + + return { + redirectTo: getUpdatedRedirectTo(appInfo, resp.redirectTo), + }; + }, + + authorization: async function (this: RecipeInterface, input) { + if (input.session !== undefined) { + if (input.params.prompt === "none") { + input.params["st_prompt"] = "none"; + delete input.params.prompt; + } + } + + const resp = await querier.sendPostRequest( + new NormalisedURLPath(`/recipe/oauth/auth`), + { + params: input.params, + cookies: `${input.cookies}`, + }, + // { + // // TODO: if session is not set also clear the oauth2 cookie + // Cookie: `${input.cookies}`, + // }, + input.userContext + ); + + if (resp.status === "OK") { + const redirectTo = getUpdatedRedirectTo(appInfo, resp.redirectTo); + if (redirectTo === undefined) { + throw new Error(resp.body); + } + const redirectToURL = new URL(redirectTo); + const consentChallenge = redirectToURL.searchParams.get("consent_challenge"); + if (consentChallenge !== null && input.session !== undefined) { + const consentRequest = await this.getConsentRequest({ + challenge: consentChallenge, + userContext: input.userContext, + }); + + const user = await getUser(input.session.getUserId()); + if (!user) { + throw new Error("Should not happen"); + } + const idToken = await this.buildIdTokenPayload({ + user, + client: consentRequest.client!, + sessionHandle: input.session.getHandle(), + scopes: consentRequest.requestedScope || [], + userContext: input.userContext, + }); + const accessTokenPayload = await this.buildAccessTokenPayload({ + user, + client: consentRequest.client!, + sessionHandle: input.session.getHandle(), + scopes: consentRequest.requestedScope || [], + userContext: input.userContext, + }); + + const sessionInfo = await getSessionInformation(input.session.getHandle()); + if (!sessionInfo) { + throw new Error("Session not found"); + } + + const consentRes = await this.acceptConsentRequest({ + ...input, + challenge: consentRequest.challenge, + grantAccessTokenAudience: consentRequest.requestedAccessTokenAudience, + grantScope: consentRequest.requestedScope, + remember: true, // TODO: verify that we need this + session: { + id_token: idToken, + access_token: accessTokenPayload, + }, + handledAt: new Date(sessionInfo.timeCreated).toISOString(), + }); + + return { + redirectTo: consentRes.redirectTo, + setCookie: resp.cookies ?? undefined, + }; + } + return { redirectTo, setCookie: resp.cookies ?? undefined }; + } + + return resp; + }, + + tokenExchange: async function (this: RecipeInterface, input) { + const inputBody: any = {}; + for (const key in input.body) { + inputBody[key] = input.body[key]; + } + + if (input.body.grant_type === "refresh_token") { + const scopes = input.body.scope?.split(" ") ?? []; + const tokenInfo = await this.introspectToken({ + token: input.body.refresh_token!, + scopes, + userContext: input.userContext, + }); + + if (tokenInfo.active === true) { + const sessionHandle = (tokenInfo.ext as any).sessionHandle as string; + + const clientInfo = await this.getOAuth2Client({ + clientId: tokenInfo.client_id as string, + userContext: input.userContext, + }); + if (clientInfo.status === "ERROR") { + return { + statusCode: 400, + error: clientInfo.error, + errorDescription: clientInfo.errorHint, + }; + } + const client = clientInfo.client; + const user = await getUser(tokenInfo.sub as string); + if (!user) { + throw new Error("User not found"); + } + const idToken = await this.buildIdTokenPayload({ + user, + client, + sessionHandle: sessionHandle, + scopes, + userContext: input.userContext, + }); + const accessTokenPayload = await this.buildAccessTokenPayload({ + user, + client, + sessionHandle: sessionHandle, + scopes, + userContext: input.userContext, + }); + inputBody["session"] = { + id_token: idToken, + access_token: accessTokenPayload, + }; + + saveTokensForHook(sessionHandle, idToken, accessTokenPayload); + } + } + + if (input.authorizationHeader) { + inputBody["authorizationHeader"] = input.authorizationHeader; + } + + const res = await querier.sendPostRequest( + new NormalisedURLPath(`/recipe/oauth/token`), + { inputBody, iss: await this.getIssuer({ userContext: input.userContext }) }, + input.userContext + ); + + if (res.status !== "OK") { + return { + statusCode: res.statusCode, + error: res.error, + errorDescription: res.errorDescription, + }; + } + + return res; + }, + + getOAuth2Clients: async function (input) { + let response = await querier.sendGetRequestWithResponseHeaders( + new NormalisedURLPath(`/recipe/oauth2/admin/clients`), + { + ...transformObjectKeys(input, "snake-case"), + page_token: input.paginationToken, + }, + {}, + input.userContext + ); + + if (response.body.status === "OK") { + // Pagination info is in the Link header, containing comma-separated links: + // "first", "next" (if applicable). + // Example: Link: ; rel="first", ; rel="next" + + // We parse the nextPaginationToken from the Link header using RegExp + let nextPaginationToken: string | undefined; + const linkHeader = response.headers.get("link") ?? ""; + + const nextLinkMatch = linkHeader.match(/<([^>]+)>;\s*rel="next"/); + if (nextLinkMatch) { + const url = nextLinkMatch[1]; + const urlParams = new URLSearchParams(url.split("?")[1]); + nextPaginationToken = urlParams.get("page_token") as string; + } + + return { + status: "OK", + clients: response.body.data.map((client: any) => OAuth2Client.fromAPIResponse(client)), + nextPaginationToken, + }; + } else { + return { + status: "ERROR", + error: response.body.data.error, + errorHint: response.body.data.errorHint, + }; + } + }, + getOAuth2Client: async function (input) { + let response = await querier.sendGetRequestWithResponseHeaders( + new NormalisedURLPath(`/recipe/oauth2/admin/clients/${input.clientId}`), + {}, + {}, + input.userContext + ); + + if (response.body.status === "OK") { + return { + status: "OK", + client: OAuth2Client.fromAPIResponse(response.body.data), + }; + } else { + return { + status: "ERROR", + error: response.body.data.error, + errorHint: response.body.data.errorHint, + }; + } + }, + createOAuth2Client: async function (input) { + let response = await querier.sendPostRequest( + new NormalisedURLPath(`/recipe/oauth2/admin/clients`), + { + ...transformObjectKeys(input, "snake-case"), + // TODO: these defaults should be set/enforced on the core side + access_token_strategy: "jwt", + skip_consent: true, + subject_type: "public", + }, + input.userContext + ); + + if (response.status === "OK") { + return { + status: "OK", + client: OAuth2Client.fromAPIResponse(response.data), + }; + } else { + return { + status: "ERROR", + error: response.data.error, + errorHint: response.data.errorHint, + }; + } + }, + updateOAuth2Client: async function (input) { + // We convert the input into an array of "replace" operations + const requestBody = Object.entries(input).reduce< + Array<{ from: string; op: "replace"; path: string; value: any }> + >((result, [key, value]) => { + result.push({ + from: `/${toSnakeCase(key)}`, + op: "replace", + path: `/${toSnakeCase(key)}`, + value, + }); + return result; + }, []); + + let response = await querier.sendPatchRequest( + new NormalisedURLPath(`/recipe/oauth2/admin/clients/${input.clientId}`), + requestBody, + input.userContext + ); + + if (response.status === "OK") { + return { + status: "OK", + client: OAuth2Client.fromAPIResponse(response.data), + }; + } else { + return { + status: "ERROR", + error: response.data.error, + errorHint: response.data.errorHint, + }; + } + }, + deleteOAuth2Client: async function (input) { + let response = await querier.sendDeleteRequest( + new NormalisedURLPath(`/recipe/oauth2/admin/clients/${input.clientId}`), + undefined, + undefined, + input.userContext + ); + + if (response.status === "OK") { + return { status: "OK" }; + } else { + return { + status: "ERROR", + error: response.data.error, + errorHint: response.data.errorHint, + }; + } + }, + getIssuer: async function (_) { + return appInfo.apiDomain.getAsStringDangerous() + appInfo.apiBasePath.getAsStringDangerous(); + }, + buildAccessTokenPayload: async function (input) { + return getDefaultAccessTokenPayload(input.user, input.scopes, input.sessionHandle, input.userContext); + }, + buildIdTokenPayload: async function (input) { + return getDefaultIdTokenPayload(input.user, input.scopes, input.sessionHandle, input.userContext); + }, + buildUserInfo: async function ({ user, accessTokenPayload, scopes, tenantId, userContext }) { + return getDefaultUserInfoPayload(user, accessTokenPayload, scopes, tenantId, userContext); + }, + validateOAuth2AccessToken: async function (input) { + const payload = (await jose.jwtVerify(input.token, getCombinedJWKS())).payload; + + // if (payload.stt !== 1) { + // throw new Error("Wrong token type"); + // } + + // TODO: we should be able uncomment this after we get proper core support + // TODO: make this configurable? + // const expectedIssuer = + // appInfo.apiDomain.getAsStringDangerous() + appInfo.apiBasePath.getAsStringDangerous(); + // if (payload.iss !== expectedIssuer) { + // throw new Error("Issuer mismatch: this token was likely issued by another application or spoofed"); + // } + + if (input.requirements?.clientId !== undefined && payload.client_id !== input.requirements.clientId) { + throw new Error("The token doesn't belong to the specified client"); + } + + if ( + input.requirements?.scopes !== undefined && + input.requirements.scopes.some((scope) => !(payload.scp as string[]).includes(scope)) + ) { + throw new Error("The token is missing some required scopes"); + } + + const aud = payload.aud instanceof Array ? payload.aud : payload.aud?.split(" ") ?? []; + if (input.requirements?.audience !== undefined && !aud.includes(input.requirements.audience)) { + throw new Error("The token doesn't belong to the specified audience"); + } + + if (input.checkDatabase) { + let response = await querier.sendPostRequest( + new NormalisedURLPath(`/recipe/oauth2/admin/oauth2/introspect`), + { + $isFormData: true, + token: input.token, + }, + input.userContext + ); + + // TODO: fix after the core interface is there + if (response.status !== "OK" || response.data.active !== true) { + throw new Error(response.data.error); + } + } + return { status: "OK", payload: payload as JSONObject }; + }, + revokeToken: async function (this: RecipeInterface, input) { + const requestBody: Record = { + $isFormData: true, + token: input.token, + }; + + if ("authorizationHeader" in input && input.authorizationHeader !== undefined) { + requestBody.authorizationHeader = input.authorizationHeader; + } else { + if ("clientId" in input && input.clientId !== undefined) { + requestBody.client_id = input.clientId; + } + if ("clientSecret" in input && input.clientSecret !== undefined) { + requestBody.client_secret = input.clientSecret; + } + } + + const res = await querier.sendPostRequest( + new NormalisedURLPath(`/recipe/oauth2/pub/revoke`), + requestBody, + input.userContext + ); + + if (res.status !== "OK") { + return { + status: "OAUTH_ERROR", + statusCode: res.statusCode, + error: res.data.error, + errorDescription: res.data.error_description, + }; + } + + return { status: "OK" }; + }, + + introspectToken: async function (this: RecipeInterface, { token, scopes, userContext }) { + // Determine if the token is an access token by checking if it doesn't start with "ory_rt" + const isAccessToken = !token.startsWith("ory_rt"); + + // Attempt to validate the access token locally + // If it fails, the token is not active, and we return early + if (isAccessToken) { + try { + await this.validateOAuth2AccessToken({ + token, + requirements: { scopes }, + checkDatabase: false, + userContext, + }); + } catch (error) { + return { active: false }; + } + } + + // For tokens that passed local validation or if it's a refresh token, + // validate the token with the database by calling the core introspection endpoint + const res = await querier.sendPostRequest( + new NormalisedURLPath(`/recipe/oauth2/admin/oauth2/introspect`), + { + $isFormData: true, + token, + scope: scopes ? scopes.join(" ") : undefined, + }, + userContext + ); + + return res.data; + }, + }; +} diff --git a/lib/ts/recipe/oauth2provider/types.ts b/lib/ts/recipe/oauth2provider/types.ts new file mode 100644 index 000000000..6784056d5 --- /dev/null +++ b/lib/ts/recipe/oauth2provider/types.ts @@ -0,0 +1,555 @@ +/* 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 type { BaseRequest, BaseResponse } from "../../framework"; +import OverrideableBuilder from "supertokens-js-override"; +import { GeneralErrorResponse, JSONObject, JSONValue, NonNullableProperties, UserContext } from "../../types"; +import { SessionContainerInterface } from "../session/types"; +import { OAuth2Client } from "./OAuth2Client"; +import { User } from "../../user"; + +export type TypeInput = { + // TODO: issuer? + override?: { + functions?: ( + originalImplementation: RecipeInterface, + builder?: OverrideableBuilder + ) => RecipeInterface; + apis?: (originalImplementation: APIInterface, builder?: OverrideableBuilder) => APIInterface; + }; +}; + +export type TypeNormalisedInput = { + override: { + functions: ( + originalImplementation: RecipeInterface, + builder?: OverrideableBuilder + ) => RecipeInterface; + apis: (originalImplementation: APIInterface, builder?: OverrideableBuilder) => APIInterface; + }; +}; + +export type APIOptions = { + recipeImplementation: RecipeInterface; + config: TypeNormalisedInput; + recipeId: string; + isInServerlessEnv: boolean; + req: BaseRequest; + res: BaseResponse; +}; + +export type ErrorOAuth2 = { + status: "OAUTH_ERROR"; + + // The error should follow the OAuth2 error format (e.g. invalid_request, login_required). + // Defaults to request_denied. + error: string; + + // Description of the error in a human readable format. + errorDescription: string; + + errorDebug?: string; + + errorHint?: string; + + // Represents the HTTP status code of the error (e.g. 401 or 403) + // Defaults to 400 + statusCode?: number; +}; + +export type ConsentRequest = { + // ACR represents the Authentication AuthorizationContext Class Reference value for this authentication session. You can use it to express that, for example, a user authenticated using two factor authentication. + acr?: string; + + // Array of strings (StringSliceJSONFormat represents []string{} which is encoded to/from JSON for SQL storage.) + amr?: string[]; + + // ID is the identifier ("authorization challenge") of the consent authorization request. It is used to identify the session. + challenge: string; + + // OAuth 2.0 Clients are used to perform OAuth 2.0 and OpenID Connect flows. Usually, OAuth 2.0 clients are generated for applications which want to consume your OAuth 2.0 or OpenID Connect capabilities. + client?: OAuth2Client; + + // any (JSONRawMessage represents a json.RawMessage that works well with JSON, SQL, and Swagger.) + context?: JSONObject; + + // LoginChallenge is the login challenge this consent challenge belongs to. It can be used to associate a login and consent request in the login & consent app. + loginChallenge?: string; + + // LoginSessionID is the login session ID. If the user-agent reuses a login session (via cookie / remember flag) this ID will remain the same. If the user-agent did not have an existing authentication session (e.g. remember is false) this will be a new random value. This value is used as the "sid" parameter in the ID Token and in OIDC Front-/Back- channel logout. It's value can generally be used to associate consecutive login requests by a certain user. + loginSessionId?: string; + + // object (Contains optional information about the OpenID Connect request.) + oidcContext?: any; + + // Array of strings (StringSliceJSONFormat represents []string{} which is encoded to/from JSON for SQL storage.) + requestedAccessTokenAudience?: string[]; + + // Array of strings (StringSliceJSONFormat represents []string{} which is encoded to/from JSON for SQL storage.) + requestedScope?: string[]; + + // Skip, if true, implies that the client has requested the same scopes from the same user previously. If true, you must not ask the user to grant the requested scopes. You must however either allow or deny the consent request using the usual API call. + skip?: boolean; + + // Subject is the user ID of the end-user that authenticated. Now, that end user needs to grant or deny the scope requested by the OAuth 2.0 client. + subject?: string; +}; + +export type LoginRequest = { + // ID is the identifier ("login challenge") of the login request. It is used to identify the session. + challenge: string; + + // OAuth 2.0 Clients are used to perform OAuth 2.0 and OpenID Connect flows. Usually, OAuth 2.0 clients are generated for applications which want to consume your OAuth 2.0 or OpenID Connect capabilities. + client: OAuth2Client; + + // object (Contains optional information about the OpenID Connect request.) + oidcContext?: any; + + // RequestURL is the original OAuth 2.0 Authorization URL requested by the OAuth 2.0 client. It is the URL which initiates the OAuth 2.0 Authorization Code or OAuth 2.0 Implicit flow. This URL is typically not needed, but might come in handy if you want to deal with additional request parameters. + requestUrl: string; + + // Array of strings (StringSliceJSONFormat represents []string{} which is encoded to/from JSON for SQL storage.) + requestedAccessTokenAudience?: string[]; + + // Array of strings (StringSliceJSONFormat represents []string{} which is encoded to/from JSON for SQL storage.) + requestedScope?: string[]; + + // SessionID is the login session ID. If the user-agent reuses a login session (via cookie / remember flag) this ID will remain the same. If the user-agent did not have an existing authentication session (e.g. remember is false) this will be a new random value. This value is used as the "sid" parameter in the ID Token and in OIDC Front-/Back- channel logout. It's value can generally be used to associate consecutive login requests by a certain user. + sessionId?: string; + + // Skip, if true, implies that the client has requested the same scopes from the same user previously. If true, you can skip asking the user to grant the requested scopes, and simply forward the user to the redirect URL. + // This feature allows you to update / set session information. + skip: boolean; + + // Subject is the user ID of the end-user that authenticated. Now, that end user needs to grant or deny the scope requested by the OAuth 2.0 client. If this value is set and skip is true, you MUST include this subject type when accepting the login request, or the request will fail. + subject: string; +}; + +export type TokenInfo = { + // The access token issued by the authorization server. + access_token?: string; + // The lifetime in seconds of the access token. For example, the value "3600" denotes that the access token will expire in one hour from the time the response was generated. + // integer + expires_in: number; + // To retrieve a refresh token request the id_token scope. + id_token?: string; + // The refresh token, which can be used to obtain new access tokens. To retrieve it add the scope "offline" to your access token request. + refresh_token?: string; + // The scope of the access token + scope: string; + // The type of the token issued + token_type: string; +}; + +export type LoginInfo = { + clientId: string; + // The name of the client. + clientName: string; + // The URI of the client's terms of service. + tosUri?: string; + // The URI of the client's privacy policy. + policyUri?: string; + // The URI of the client's logo. + logoUri?: string; + // The URI of the client + clientUri?: string; + // The metadata associated with the client. + metadata?: Record | null; +}; + +export type UserInfo = { + sub: string; + email?: string; + email_verified?: boolean; + phoneNumber?: string; + phoneNumber_verified?: boolean; + [key: string]: JSONValue; +}; + +export type InstrospectTokenResponse = { active: false } | ({ active: true } & JSONObject); + +export type RecipeInterface = { + authorization(input: { + params: Record; + cookies: string | undefined; + session: SessionContainerInterface | undefined; + userContext: UserContext; + }): Promise<{ status: "OK"; redirectTo: string; setCookie: string | undefined } | ErrorOAuth2>; + + tokenExchange(input: { + authorizationHeader?: string; + body: Record; + userContext: UserContext; + }): Promise; + + getConsentRequest(input: { challenge: string; userContext: UserContext }): Promise; + acceptConsentRequest(input: { + challenge: string; + + // any (JSONRawMessage represents a json.RawMessage that works well with JSON, SQL, and Swagger.) + context?: any; + // Array of strings (StringSliceJSONFormat represents []string{} which is encoded to/from JSON for SQL storage.) + grantAccessTokenAudience?: string[]; + // Array of strings (StringSliceJSONFormat represents []string{} which is encoded to/from JSON for SQL storage.) + grantScope?: string[]; + // string (NullTime implements sql.NullTime functionality.) + handledAt?: string; + // Remember, if set to true, tells ORY Hydra to remember this consent authorization and reuse it if the same client asks the same user for the same, or a subset of, scope. + remember?: boolean; + + // RememberFor sets how long the consent authorization should be remembered for in seconds. If set to 0, the authorization will be remembered indefinitely. integer + rememberFor?: number; + + // object (Pass session data to a consent request.) + session?: any; + userContext: UserContext; + }): Promise<{ redirectTo: string }>; + + rejectConsentRequest(input: { + challenge: string; + error: ErrorOAuth2; + userContext: UserContext; + }): Promise<{ redirectTo: string }>; + + getLoginRequest(input: { challenge: string; userContext: UserContext }): Promise; + acceptLoginRequest(input: { + challenge: string; + + // ACR sets the Authentication AuthorizationContext Class Reference value for this authentication session. You can use it to express that, for example, a user authenticated using two factor authentication. + acr?: string; + + // Array of strings (StringSliceJSONFormat represents []string{} which is encoded to/from JSON for SQL storage.) + amr?: string[]; + + // any (JSONRawMessage represents a json.RawMessage that works well with JSON, SQL, and Swagger.) + context?: any; + + // Extend OAuth2 authentication session lifespan + // If set to true, the OAuth2 authentication cookie lifespan is extended. This is for example useful if you want the user to be able to use prompt=none continuously. + // This value can only be set to true if the user has an authentication, which is the case if the skip value is true. + extendSessionLifespan?: boolean; + + // ForceSubjectIdentifier forces the "pairwise" user ID of the end-user that authenticated. The "pairwise" user ID refers to the (Pairwise Identifier Algorithm)[http://openid.net/specs/openid-connect-core-1_0.html#PairwiseAlg] of the OpenID Connect specification. It allows you to set an obfuscated subject ("user") identifier that is unique to the client. + // Please note that this changes the user ID on endpoint /userinfo and sub claim of the ID Token. It does not change the sub claim in the OAuth 2.0 Introspection. + forceSubjectIdentifier?: string; + + // Per default, ORY Hydra handles this value with its own algorithm. In case you want to set this yourself you can use this field. Please note that setting this field has no effect if pairwise is not configured in ORY Hydra or the OAuth 2.0 Client does not expect a pairwise identifier (set via subject_type key in the client's configuration). + // Please also be aware that ORY Hydra is unable to properly compute this value during authentication. This implies that you have to compute this value on every authentication process (probably depending on the client ID or some other unique value). + // If you fail to compute the proper value, then authentication processes which have id_token_hint set might fail. + + // IdentityProviderSessionID is the session ID of the end-user that authenticated. If specified, we will use this value to propagate the logout. + identityProviderSessionId?: string; + + // Remember, if set to true, tells ORY Hydra to remember this user by telling the user agent (browser) to store a cookie with authentication data. If the same user performs another OAuth 2.0 Authorization Request, he/she will not be asked to log in again. + remember?: boolean; + + // RememberFor sets how long the authentication should be remembered for in seconds. If set to 0, the authorization will be remembered for the duration of the browser session (using a session cookie). integer + rememberFor?: number; + + // Subject is the user ID of the end-user that authenticated. + subject: string; + userContext: UserContext; + }): Promise<{ redirectTo: string }>; + rejectLoginRequest(input: { + challenge: string; + error: ErrorOAuth2; + userContext: UserContext; + }): Promise<{ redirectTo: string }>; + + getOAuth2Client(input: { + clientId: string; + userContext: UserContext; + }): Promise< + | { + status: "OK"; + client: OAuth2Client; + } + // TODO: Define specific error types once requirements are clearer + | { + status: "ERROR"; + error: string; + errorHint: string; + } + >; + getOAuth2Clients( + input: GetOAuth2ClientsInput & { + userContext: UserContext; + } + ): Promise< + | { + status: "OK"; + clients: Array; + nextPaginationToken?: string; + } + // TODO: Define specific error types once requirements are clearer + | { + status: "ERROR"; + error: string; + errorHint: string; + } + >; + createOAuth2Client( + input: CreateOAuth2ClientInput & { + userContext: UserContext; + } + ): Promise< + | { + status: "OK"; + client: OAuth2Client; + } + // TODO: Define specific error types once requirements are clearer + | { + status: "ERROR"; + error: string; + errorHint: string; + } + >; + updateOAuth2Client( + input: UpdateOAuth2ClientInput & { + userContext: UserContext; + } + ): Promise< + | { + status: "OK"; + client: OAuth2Client; + } + // TODO: Define specific error types once requirements are clearer + | { + status: "ERROR"; + error: string; + errorHint: string; + } + >; + deleteOAuth2Client( + input: DeleteOAuth2ClientInput & { + userContext: UserContext; + } + ): Promise< + | { + status: "OK"; + } + // TODO: Define specific error types once requirements are clearer + | { + status: "ERROR"; + error: string; + errorHint: string; + } + >; + + validateOAuth2AccessToken(input: { + token: string; + requirements?: { + clientId?: string; + scopes?: string[]; + audience?: string; + }; + checkDatabase?: boolean; + userContext: UserContext; + }): Promise<{ status: "OK"; payload: JSONObject }>; + + getIssuer(input: { userContext: UserContext }): Promise; + + buildAccessTokenPayload(input: { + user: User; + client: OAuth2Client; + sessionHandle: string; + scopes: string[]; + userContext: UserContext; + }): Promise; + buildIdTokenPayload(input: { + user: User; + client: OAuth2Client; + sessionHandle: string; + scopes: string[]; + userContext: UserContext; + }): Promise; + buildUserInfo(input: { + user: User; + accessTokenPayload: JSONObject; + scopes: string[]; + tenantId: string; + userContext: UserContext; + }): Promise; + revokeToken( + input: { + token: string; + userContext: UserContext; + } & ( + | { + authorizationHeader: string; + } + | { clientId: string; clientSecret?: string } + ) + ): Promise<{ status: "OK" } | ErrorOAuth2>; + introspectToken(input: { + token: string; + scopes?: string[]; + userContext: UserContext; + }): Promise; +}; + +export type APIInterface = { + loginGET: + | undefined + | ((input: { + loginChallenge: string; + options: APIOptions; + session?: SessionContainerInterface; + shouldTryRefresh: boolean; + userContext: UserContext; + }) => Promise<{ redirectTo: string; setCookie: string | undefined } | GeneralErrorResponse>); + + authGET: + | undefined + | ((input: { + params: any; + cookie: string | undefined; + session: SessionContainerInterface | undefined; + shouldTryRefresh: boolean; + options: APIOptions; + userContext: UserContext; + }) => Promise<{ redirectTo: string; setCookie: string | undefined } | ErrorOAuth2 | GeneralErrorResponse>); + tokenPOST: + | undefined + | ((input: { + authorizationHeader?: string; + body: any; + options: APIOptions; + userContext: UserContext; + }) => Promise); + loginInfoGET: + | undefined + | ((input: { + loginChallenge: string; + options: APIOptions; + userContext: UserContext; + }) => Promise<{ status: "OK"; info: LoginInfo } | GeneralErrorResponse>); + userInfoGET: + | undefined + | ((input: { + accessTokenPayload: JSONObject; + user: User; + scopes: string[]; + tenantId: string; + options: APIOptions; + userContext: UserContext; + }) => Promise); + revokeTokenPOST: + | undefined + | (( + input: { + token: string; + options: APIOptions; + userContext: UserContext; + } & ({ authorizationHeader: string } | { clientId: string; clientSecret?: string }) + ) => Promise<{ status: "OK" } | ErrorOAuth2>); + introspectTokenPOST: + | undefined + | ((input: { + token: string; + scopes?: string[]; + options: APIOptions; + userContext: UserContext; + }) => Promise); +}; + +export type OAuth2ClientOptions = { + clientId: string; + clientSecret?: string; + createdAt: string; + updatedAt: string; + + clientName: string; + + scope: string; + redirectUris?: string[] | null; + allowedCorsOrigins?: string[]; + + authorizationCodeGrantAccessTokenLifespan?: string | null; + authorizationCodeGrantIdTokenLifespan?: string | null; + authorizationCodeGrantRefreshTokenLifespan?: string | null; + clientCredentialsGrantAccessTokenLifespan?: string | null; + implicitGrantAccessTokenLifespan?: string | null; + implicitGrantIdTokenLifespan?: string | null; + refreshTokenGrantAccessTokenLifespan?: string | null; + refreshTokenGrantIdTokenLifespan?: string | null; + refreshTokenGrantRefreshTokenLifespan?: string | null; + + tokenEndpointAuthMethod: string; + + audience?: string[]; + grantTypes?: string[] | null; + responseTypes?: string[] | null; + + clientUri?: string; + logoUri?: string; + policyUri?: string; + tosUri?: string; + metadata?: Record; +}; + +export type GetOAuth2ClientsInput = { + /** + * Items per Page. Defaults to 250. + */ + pageSize?: number; + + /** + * Next Page Token. Defaults to "1". + */ + paginationToken?: string; + + /** + * The name of the clients to filter by. + */ + clientName?: string; + + /** + * The owner of the clients to filter by. + */ + owner?: string; +}; + +export type CreateOAuth2ClientInput = Partial< + Omit +>; + +export type UpdateOAuth2ClientInput = NonNullableProperties< + Omit +> & { + clientId: string; + redirectUris?: string[] | null; + grantTypes?: string[] | null; + responseTypes?: string[] | null; + metadata?: Record | null; +}; + +export type DeleteOAuth2ClientInput = { + clientId: string; +}; + +export type PayloadBuilderFunction = ( + user: User, + scopes: string[], + sessionHandle: string, + userContext: UserContext +) => Promise; +export type UserInfoBuilderFunction = ( + user: User, + accessTokenPayload: JSONObject, + scopes: string[], + tenantId: string, + userContext: UserContext +) => Promise; diff --git a/lib/ts/recipe/oauth2provider/utils.ts b/lib/ts/recipe/oauth2provider/utils.ts new file mode 100644 index 000000000..d59cd4537 --- /dev/null +++ b/lib/ts/recipe/oauth2provider/utils.ts @@ -0,0 +1,34 @@ +/* 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 { NormalisedAppinfo } from "../../types"; +import Recipe from "./recipe"; +import { APIInterface, RecipeInterface, TypeInput, TypeNormalisedInput } from "./types"; + +export function validateAndNormaliseUserInput( + _: Recipe, + __: NormalisedAppinfo, + config?: TypeInput +): TypeNormalisedInput { + let override = { + functions: (originalImplementation: RecipeInterface) => originalImplementation, + apis: (originalImplementation: APIInterface) => originalImplementation, + ...config?.override, + }; + + return { + override, + }; +} diff --git a/lib/ts/recipe/openid/api/getOpenIdDiscoveryConfiguration.ts b/lib/ts/recipe/openid/api/getOpenIdDiscoveryConfiguration.ts index 90c291574..169589677 100644 --- a/lib/ts/recipe/openid/api/getOpenIdDiscoveryConfiguration.ts +++ b/lib/ts/recipe/openid/api/getOpenIdDiscoveryConfiguration.ts @@ -34,6 +34,14 @@ export default async function getOpenIdDiscoveryConfiguration( send200Response(options.res, { issuer: result.issuer, jwks_uri: result.jwks_uri, + authorization_endpoint: result.authorization_endpoint, + token_endpoint: result.token_endpoint, + userinfo_endpoint: result.userinfo_endpoint, + revocation_endpoint: result.revocation_endpoint, + token_introspection_endpoint: result.token_introspection_endpoint, + subject_types_supported: result.subject_types_supported, + id_token_signing_alg_values_supported: result.id_token_signing_alg_values_supported, + response_types_supported: result.response_types_supported, }); } else { send200Response(options.res, result); diff --git a/lib/ts/recipe/openid/api/implementation.ts b/lib/ts/recipe/openid/api/implementation.ts index ee1f83e21..72921dffd 100644 --- a/lib/ts/recipe/openid/api/implementation.ts +++ b/lib/ts/recipe/openid/api/implementation.ts @@ -12,18 +12,11 @@ * License for the specific language governing permissions and limitations * under the License. */ -import { APIInterface, APIOptions } from "../types"; -import { GeneralErrorResponse, UserContext } from "../../../types"; +import { APIInterface } from "../types"; export default function getAPIImplementation(): APIInterface { return { - getOpenIdDiscoveryConfigurationGET: async function ({ - options, - userContext, - }: { - options: APIOptions; - userContext: UserContext; - }): Promise<{ status: "OK"; issuer: string; jwks_uri: string } | GeneralErrorResponse> { + getOpenIdDiscoveryConfigurationGET: async function ({ options, userContext }) { return await options.recipeImplementation.getOpenIdDiscoveryConfiguration({ userContext }); }, }; diff --git a/lib/ts/recipe/openid/recipe.ts b/lib/ts/recipe/openid/recipe.ts index 1dc802de5..1726f9c43 100644 --- a/lib/ts/recipe/openid/recipe.ts +++ b/lib/ts/recipe/openid/recipe.ts @@ -44,7 +44,9 @@ export default class OpenIdRecipe extends RecipeModule { override: this.config.override.jwtFeature, }); - let builder = new OverrideableBuilder(RecipeImplementation(this.config, this.jwtRecipe.recipeInterfaceImpl)); + let builder = new OverrideableBuilder( + RecipeImplementation(this.config, this.jwtRecipe.recipeInterfaceImpl, appInfo) + ); this.recipeImplementation = builder.override(this.config.override.functions).build(); diff --git a/lib/ts/recipe/openid/recipeImplementation.ts b/lib/ts/recipe/openid/recipeImplementation.ts index f161e8d23..ce91ede5e 100644 --- a/lib/ts/recipe/openid/recipeImplementation.ts +++ b/lib/ts/recipe/openid/recipeImplementation.ts @@ -16,26 +16,40 @@ 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"; +import { NormalisedAppinfo, UserContext } from "../../types"; +import { + AUTH_PATH, + INTROSPECT_TOKEN_PATH, + REVOKE_TOKEN_PATH, + TOKEN_PATH, + USER_INFO_PATH, +} from "../oauth2provider/constants"; export default function getRecipeInterface( config: TypeNormalisedInput, - jwtRecipeImplementation: JWTRecipeInterface + jwtRecipeImplementation: JWTRecipeInterface, + appInfo: NormalisedAppinfo ): RecipeInterface { return { - getOpenIdDiscoveryConfiguration: async function (): Promise<{ - status: "OK"; - issuer: string; - jwks_uri: string; - }> { + getOpenIdDiscoveryConfiguration: async function () { let issuer = config.issuerDomain.getAsStringDangerous() + config.issuerPath.getAsStringDangerous(); let jwks_uri = config.issuerDomain.getAsStringDangerous() + config.issuerPath.appendPath(new NormalisedURLPath(GET_JWKS_API)).getAsStringDangerous(); + + const apiBasePath = appInfo.apiDomain.getAsStringDangerous() + appInfo.apiBasePath.getAsStringDangerous(); return { status: "OK", issuer, jwks_uri, + authorization_endpoint: apiBasePath + AUTH_PATH, + token_endpoint: apiBasePath + TOKEN_PATH, + userinfo_endpoint: apiBasePath + USER_INFO_PATH, + revocation_endpoint: apiBasePath + REVOKE_TOKEN_PATH, + token_introspection_endpoint: apiBasePath + INTROSPECT_TOKEN_PATH, + subject_types_supported: ["public"], + id_token_signing_alg_values_supported: ["RS256"], + response_types_supported: ["code", "id_token", "id_token token"], }; }, createJWT: async function ({ diff --git a/lib/ts/recipe/openid/types.ts b/lib/ts/recipe/openid/types.ts index c303cb0ca..51702f454 100644 --- a/lib/ts/recipe/openid/types.ts +++ b/lib/ts/recipe/openid/types.ts @@ -83,6 +83,14 @@ export type APIInterface = { status: "OK"; issuer: string; jwks_uri: string; + authorization_endpoint: string; + token_endpoint: string; + userinfo_endpoint: string; + revocation_endpoint: string; + token_introspection_endpoint: string; + subject_types_supported: string[]; + id_token_signing_alg_values_supported: string[]; + response_types_supported: string[]; } | GeneralErrorResponse >); @@ -95,6 +103,14 @@ export type RecipeInterface = { status: "OK"; issuer: string; jwks_uri: string; + authorization_endpoint: string; + token_endpoint: string; + userinfo_endpoint: string; + revocation_endpoint: string; + token_introspection_endpoint: string; + subject_types_supported: string[]; + id_token_signing_alg_values_supported: string[]; + response_types_supported: string[]; }>; createJWT(input: { payload?: any; diff --git a/lib/ts/recipe/passwordless/api/consumeCode.ts b/lib/ts/recipe/passwordless/api/consumeCode.ts index 38aa06c4b..39229cca7 100644 --- a/lib/ts/recipe/passwordless/api/consumeCode.ts +++ b/lib/ts/recipe/passwordless/api/consumeCode.ts @@ -13,11 +13,15 @@ * under the License. */ -import { getBackwardsCompatibleUserInfo, send200Response } from "../../../utils"; +import { + getBackwardsCompatibleUserInfo, + getNormalisedShouldTryLinkingWithSessionUserFlag, + send200Response, +} from "../../../utils"; import STError from "../error"; import { APIInterface, APIOptions } from ".."; import { UserContext } from "../../../types"; -import Session from "../../session"; +import { AuthUtils } from "../../../authUtils"; export default async function consumeCode( apiImplementation: APIInterface, @@ -62,13 +66,12 @@ export default async function consumeCode( }); } - let session = await Session.getSession( + const shouldTryLinkingWithSessionUser = getNormalisedShouldTryLinkingWithSessionUserFlag(options.req, body); + + const session = await AuthUtils.loadSessionInAuthAPIIfNeeded( options.req, options.res, - { - sessionRequired: false, - overrideGlobalClaimValidators: () => [], - }, + shouldTryLinkingWithSessionUser, userContext ); @@ -84,6 +87,7 @@ export default async function consumeCode( preAuthSessionId, tenantId, session, + shouldTryLinkingWithSessionUser, options, userContext, } @@ -93,6 +97,7 @@ export default async function consumeCode( preAuthSessionId, tenantId, session, + shouldTryLinkingWithSessionUser, userContext, } ); diff --git a/lib/ts/recipe/passwordless/api/createCode.ts b/lib/ts/recipe/passwordless/api/createCode.ts index af46ff86d..246742c8c 100644 --- a/lib/ts/recipe/passwordless/api/createCode.ts +++ b/lib/ts/recipe/passwordless/api/createCode.ts @@ -13,12 +13,12 @@ * under the License. */ -import { send200Response } from "../../../utils"; +import { getNormalisedShouldTryLinkingWithSessionUserFlag, send200Response } from "../../../utils"; import STError from "../error"; import { APIInterface, APIOptions } from ".."; import parsePhoneNumber from "libphonenumber-js/max"; import { UserContext } from "../../../types"; -import Session from "../../session"; +import { AuthUtils } from "../../../authUtils"; export default async function createCode( apiImplementation: APIInterface, @@ -93,13 +93,12 @@ export default async function createCode( } } - let session = await Session.getSession( + const shouldTryLinkingWithSessionUser = getNormalisedShouldTryLinkingWithSessionUserFlag(options.req, body); + + const session = await AuthUtils.loadSessionInAuthAPIIfNeeded( options.req, options.res, - { - sessionRequired: false, - overrideGlobalClaimValidators: () => [], - }, + shouldTryLinkingWithSessionUser, userContext ); @@ -109,8 +108,8 @@ export default async function createCode( let result = await apiImplementation.createCodePOST( email !== undefined - ? { email, session, tenantId, options, userContext } - : { phoneNumber: phoneNumber!, session, tenantId, options, userContext } + ? { email, session, tenantId, shouldTryLinkingWithSessionUser, options, userContext } + : { phoneNumber: phoneNumber!, session, tenantId, shouldTryLinkingWithSessionUser, options, userContext } ); send200Response(options.res, result); diff --git a/lib/ts/recipe/passwordless/api/implementation.ts b/lib/ts/recipe/passwordless/api/implementation.ts index 7b7894bb8..c769ad6ea 100644 --- a/lib/ts/recipe/passwordless/api/implementation.ts +++ b/lib/ts/recipe/passwordless/api/implementation.ts @@ -179,6 +179,7 @@ export default function getAPIImplementation(): APIInterface { tenantId: input.tenantId, userContext: input.userContext, session: input.session, + shouldTryLinkingWithSessionUser: input.shouldTryLinkingWithSessionUser, }); if (preAuthChecks.status !== "OK") { @@ -207,6 +208,7 @@ export default function getAPIImplementation(): APIInterface { deviceId: input.deviceId, userInputCode: input.userInputCode, session: input.session, + shouldTryLinkingWithSessionUser: input.shouldTryLinkingWithSessionUser, tenantId: input.tenantId, userContext: input.userContext, } @@ -214,6 +216,7 @@ export default function getAPIImplementation(): APIInterface { preAuthSessionId: input.preAuthSessionId, linkCode: input.linkCode, session: input.session, + shouldTryLinkingWithSessionUser: input.shouldTryLinkingWithSessionUser, tenantId: input.tenantId, userContext: input.userContext, } @@ -319,6 +322,7 @@ export default function getAPIImplementation(): APIInterface { factorIds, userContext: input.userContext, session: input.session, + shouldTryLinkingWithSessionUser: input.shouldTryLinkingWithSessionUser, }); if (preAuthChecks.status !== "OK") { @@ -344,6 +348,7 @@ export default function getAPIImplementation(): APIInterface { input.userContext ), session: input.session, + shouldTryLinkingWithSessionUser: input.shouldTryLinkingWithSessionUser, tenantId: input.tenantId, } : { @@ -357,12 +362,13 @@ export default function getAPIImplementation(): APIInterface { input.userContext ), session: input.session, + shouldTryLinkingWithSessionUser: input.shouldTryLinkingWithSessionUser, tenantId: input.tenantId, } ); if (response.status !== "OK") { - return AuthUtils.getErrorStatusResponseWithReason(response, {}, "SIGN_IN_UP_NOT_ALLOWED"); + return AuthUtils.getErrorStatusResponseWithReason(response, errorCodeMap, "SIGN_IN_UP_NOT_ALLOWED"); } // now we send the email / text message. @@ -500,6 +506,7 @@ export default function getAPIImplementation(): APIInterface { }); const authTypeInfo = await AuthUtils.checkAuthTypeAndLinkingStatus( input.session, + input.shouldTryLinkingWithSessionUser, { recipeId: "passwordless", email: deviceInfo.email, @@ -553,7 +560,7 @@ export default function getAPIImplementation(): APIInterface { // This mirrors how we construct factorIds in createCodePOST let factorIds; - if (input.session !== undefined) { + if (!authTypeInfo.isFirstFactor) { if (deviceInfo.email !== undefined) { factorIds = [FactorIds.OTP_EMAIL]; } else { diff --git a/lib/ts/recipe/passwordless/api/resendCode.ts b/lib/ts/recipe/passwordless/api/resendCode.ts index cb946546a..1f1abe48d 100644 --- a/lib/ts/recipe/passwordless/api/resendCode.ts +++ b/lib/ts/recipe/passwordless/api/resendCode.ts @@ -13,11 +13,11 @@ * under the License. */ -import { send200Response } from "../../../utils"; +import { getNormalisedShouldTryLinkingWithSessionUserFlag, send200Response } from "../../../utils"; import STError from "../error"; import { APIInterface, APIOptions } from ".."; import { UserContext } from "../../../types"; -import Session from "../../session"; +import { AuthUtils } from "../../../authUtils"; export default async function resendCode( apiImplementation: APIInterface, @@ -47,25 +47,21 @@ export default async function resendCode( }); } - let session = await Session.getSession( + const shouldTryLinkingWithSessionUser = getNormalisedShouldTryLinkingWithSessionUserFlag(options.req, body); + + const session = await AuthUtils.loadSessionInAuthAPIIfNeeded( options.req, options.res, - { - sessionRequired: false, - overrideGlobalClaimValidators: () => [], - }, + shouldTryLinkingWithSessionUser, userContext ); - if (session !== undefined) { - tenantId = session.getTenantId(); - } - let result = await apiImplementation.resendCodePOST({ deviceId, preAuthSessionId, tenantId, session, + shouldTryLinkingWithSessionUser, options, userContext, }); diff --git a/lib/ts/recipe/passwordless/index.ts b/lib/ts/recipe/passwordless/index.ts index 92b15e493..43d00fd2a 100644 --- a/lib/ts/recipe/passwordless/index.ts +++ b/lib/ts/recipe/passwordless/index.ts @@ -51,6 +51,7 @@ export default class Wrapper { return Recipe.getInstanceOrThrowError().recipeInterfaceImpl.createCode({ ...input, session: input.session, + shouldTryLinkingWithSessionUser: !!input.session, userContext: getUserContext(input.userContext), }); } @@ -203,6 +204,7 @@ export default class Wrapper { return Recipe.getInstanceOrThrowError().recipeInterfaceImpl.consumeCode({ ...input, session: input.session, + shouldTryLinkingWithSessionUser: !!input.session, userContext: getUserContext(input.userContext), }); } diff --git a/lib/ts/recipe/passwordless/recipe.ts b/lib/ts/recipe/passwordless/recipe.ts index 9410bfb00..5ec867387 100644 --- a/lib/ts/recipe/passwordless/recipe.ts +++ b/lib/ts/recipe/passwordless/recipe.ts @@ -577,6 +577,7 @@ export default class Recipe extends RecipeModule { email: input.email, userInputCode, session: input.session, + shouldTryLinkingWithSessionUser: !!input.session, tenantId: input.tenantId, userContext: input.userContext, } @@ -584,6 +585,7 @@ export default class Recipe extends RecipeModule { phoneNumber: input.phoneNumber, userInputCode, session: input.session, + shouldTryLinkingWithSessionUser: !!input.session, tenantId: input.tenantId, userContext: input.userContext, } @@ -635,12 +637,14 @@ export default class Recipe extends RecipeModule { email: input.email, tenantId: input.tenantId, session: input.session, + shouldTryLinkingWithSessionUser: !!input.session, userContext: input.userContext, } : { phoneNumber: input.phoneNumber, tenantId: input.tenantId, session: input.session, + shouldTryLinkingWithSessionUser: !!input.session, userContext: input.userContext, } ); @@ -655,6 +659,7 @@ export default class Recipe extends RecipeModule { preAuthSessionId: codeInfo.preAuthSessionId, linkCode: codeInfo.linkCode, session: input.session, + shouldTryLinkingWithSessionUser: !!input.session, tenantId: input.tenantId, userContext: input.userContext, } @@ -663,6 +668,7 @@ export default class Recipe extends RecipeModule { deviceId: codeInfo.deviceId, userInputCode: codeInfo.userInputCode, session: input.session, + shouldTryLinkingWithSessionUser: !!input.session, tenantId: input.tenantId, userContext: input.userContext, } diff --git a/lib/ts/recipe/passwordless/recipeImplementation.ts b/lib/ts/recipe/passwordless/recipeImplementation.ts index d157ce3b0..df4ad34c4 100644 --- a/lib/ts/recipe/passwordless/recipeImplementation.ts +++ b/lib/ts/recipe/passwordless/recipeImplementation.ts @@ -51,11 +51,12 @@ export default function getRecipeInterface(querier: Querier): RecipeInterface { // Attempt account linking (this is a sign up) let updatedUser = response.user; - const linkResult = await AuthUtils.linkToSessionIfProvidedElseCreatePrimaryUserIdOrLinkByAccountInfo({ + const linkResult = await AuthUtils.linkToSessionIfRequiredElseCreatePrimaryUserIdOrLinkByAccountInfo({ tenantId: input.tenantId, inputUser: response.user, recipeUserId: response.recipeUserId, session: input.session, + shouldTryLinkingWithSessionUser: input.shouldTryLinkingWithSessionUser, userContext: input.userContext, }); @@ -200,6 +201,7 @@ export default function getRecipeInterface(querier: Querier): RecipeInterface { let response = await querier.sendPutRequest( new NormalisedURLPath(`/recipe/user`), copyAndRemoveUserContextAndTenantId(input), + {}, input.userContext ); if (response.status !== "OK") { diff --git a/lib/ts/recipe/passwordless/types.ts b/lib/ts/recipe/passwordless/types.ts index 317d809ef..cbbf6f95f 100644 --- a/lib/ts/recipe/passwordless/types.ts +++ b/lib/ts/recipe/passwordless/types.ts @@ -119,6 +119,7 @@ export type RecipeInterface = { ) & { userInputCode?: string; session: SessionContainerInterface | undefined; + shouldTryLinkingWithSessionUser: boolean | undefined; tenantId: string; userContext: UserContext; } @@ -158,6 +159,7 @@ export type RecipeInterface = { deviceId: string; preAuthSessionId: string; session: SessionContainerInterface | undefined; + shouldTryLinkingWithSessionUser: boolean | undefined; tenantId: string; userContext: UserContext; } @@ -165,6 +167,7 @@ export type RecipeInterface = { linkCode: string; preAuthSessionId: string; session: SessionContainerInterface | undefined; + shouldTryLinkingWithSessionUser: boolean | undefined; tenantId: string; userContext: UserContext; } @@ -334,6 +337,7 @@ export type APIInterface = { input: ({ email: string } | { phoneNumber: string }) & { tenantId: string; session: SessionContainerInterface | undefined; + shouldTryLinkingWithSessionUser: boolean | undefined; options: APIOptions; userContext: UserContext; } @@ -355,6 +359,7 @@ export type APIInterface = { input: { deviceId: string; preAuthSessionId: string } & { tenantId: string; session: SessionContainerInterface | undefined; + shouldTryLinkingWithSessionUser: boolean | undefined; options: APIOptions; userContext: UserContext; } @@ -374,6 +379,7 @@ export type APIInterface = { ) & { tenantId: string; session: SessionContainerInterface | undefined; + shouldTryLinkingWithSessionUser: boolean | undefined; options: APIOptions; userContext: UserContext; } diff --git a/lib/ts/recipe/session/accessToken.ts b/lib/ts/recipe/session/accessToken.ts index c9f5bc637..8c47ef6f6 100644 --- a/lib/ts/recipe/session/accessToken.ts +++ b/lib/ts/recipe/session/accessToken.ts @@ -124,6 +124,10 @@ export async function getInfoFromAccessToken( } export function validateAccessTokenStructure(payload: any, version: number) { + if (payload.stt !== 0 && payload.stt !== undefined) { + throw Error("Wrong token type"); + } + if (version >= 5) { if ( typeof payload.sub !== "string" || diff --git a/lib/ts/recipe/session/constants.ts b/lib/ts/recipe/session/constants.ts index 7a2a78498..e7a5c296a 100644 --- a/lib/ts/recipe/session/constants.ts +++ b/lib/ts/recipe/session/constants.ts @@ -34,4 +34,5 @@ export const protectedProps = [ "antiCsrfToken", "rsub", "tId", + "stt", ]; diff --git a/lib/ts/recipe/session/recipe.ts b/lib/ts/recipe/session/recipe.ts index d73ad8ccf..57a7b42be 100644 --- a/lib/ts/recipe/session/recipe.ts +++ b/lib/ts/recipe/session/recipe.ts @@ -42,6 +42,8 @@ import OverrideableBuilder from "supertokens-js-override"; import { APIOptions } from "."; import OpenIdRecipe from "../openid/recipe"; import { logDebugMessage } from "../../logger"; +import { resetCombinedJWKS } from "../../combinedRemoteJWKSet"; +import { getAccessTokenFromRequest } from "./sessionRequestFunctions"; // For Express export default class SessionRecipe extends RecipeModule { @@ -125,6 +127,7 @@ export default class SessionRecipe extends RecipeModule { throw new Error("calling testing function in non testing env"); } SessionRecipe.instance = undefined; + resetCombinedJWKS(); } addClaimFromOtherRecipe = (claim: SessionClaim) => { @@ -282,4 +285,14 @@ export default class SessionRecipe extends RecipeModule { userContext, }); }; + + getAccessTokenFromRequest = (req: any, userContext: UserContext) => { + const allowedTransferMethod = this.config.getTokenTransferMethod({ + req, + forCreateNewSession: false, + userContext, + }); + + return getAccessTokenFromRequest(req, allowedTransferMethod); + }; } diff --git a/lib/ts/recipe/session/recipeImplementation.ts b/lib/ts/recipe/session/recipeImplementation.ts index 64c526d03..0e8e67040 100644 --- a/lib/ts/recipe/session/recipeImplementation.ts +++ b/lib/ts/recipe/session/recipeImplementation.ts @@ -1,4 +1,3 @@ -import { createRemoteJWKSet, JWTVerifyGetKey } from "jose"; import { RecipeInterface, VerifySessionOptions, @@ -22,11 +21,10 @@ import { validateAccessTokenStructure } from "./accessToken"; import SessionError from "./error"; import RecipeUserId from "../../recipeUserId"; import { DEFAULT_TENANT_ID } from "../multitenancy/constants"; -import { JWKCacheCooldownInMs, protectedProps } from "./constants"; +import { protectedProps } from "./constants"; export type Helpers = { querier: Querier; - JWKS: JWTVerifyGetKey; config: TypeNormalisedInput; appInfo: NormalisedAppinfo; getRecipeImpl: () => RecipeInterface; @@ -38,40 +36,6 @@ export default function getRecipeInterface( appInfo: NormalisedAppinfo, getRecipeImplAfterOverrides: () => RecipeInterface ): RecipeInterface { - const JWKS: ReturnType[] = querier - .getAllCoreUrlsForPath("/.well-known/jwks.json") - .map((url) => - createRemoteJWKSet(new URL(url), { - cooldownDuration: JWKCacheCooldownInMs, - cacheMaxAge: config.jwksRefreshIntervalSec * 1000, - }) - ); - - /** - This function fetches all JWKs from the first available core instance. This combines the other JWKS functions to become - error resistant. - - Every core instance a backend is connected to is expected to connect to the same database and use the same key set for - token verification. Otherwise, the result of session verification would depend on which core is currently available. - */ - const combinedJWKS: ReturnType = async (...args) => { - let lastError = undefined; - if (JWKS.length === 0) { - throw Error( - "No SuperTokens core available to query. Please pass supertokens > connectionURI to the init function, or override all the functions of the recipe you are using." - ); - } - for (const jwks of JWKS) { - try { - // We await before returning to make sure we catch the error - return await jwks(...args); - } catch (ex) { - lastError = ex; - } - } - throw lastError; - }; - let obj: RecipeInterface = { createNewSession: async function ({ recipeUserId, @@ -601,7 +565,6 @@ export default function getRecipeInterface( let helpers: Helpers = { querier, - JWKS: combinedJWKS, config, appInfo, getRecipeImpl: getRecipeImplAfterOverrides, diff --git a/lib/ts/recipe/session/sessionFunctions.ts b/lib/ts/recipe/session/sessionFunctions.ts index f02c4030e..dc05e4469 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 { UserContext } from "../../types"; +import { getCombinedJWKS } from "../../combinedRemoteJWKSet"; /** * @description call this to "login" a user. @@ -111,7 +112,7 @@ export async function getSession( */ accessTokenInfo = await getInfoFromAccessToken( parsedAccessToken, - helpers.JWKS, + getCombinedJWKS(), helpers.config.antiCsrfFunctionOrString === "VIA_TOKEN" && doAntiCsrfCheck ); } catch (err) { @@ -500,6 +501,7 @@ export async function updateSessionDataInDatabase( sessionHandle, userDataInDatabase: newSessionData, }, + {}, userContext ); if (response.status === "UNAUTHORISED") { @@ -522,6 +524,7 @@ export async function updateAccessTokenPayload( sessionHandle, userDataInJWT: newAccessTokenPayload, }, + {}, userContext ); if (response.status === "UNAUTHORISED") { diff --git a/lib/ts/recipe/session/sessionRequestFunctions.ts b/lib/ts/recipe/session/sessionRequestFunctions.ts index de4c5b3bb..c6cb64158 100644 --- a/lib/ts/recipe/session/sessionRequestFunctions.ts +++ b/lib/ts/recipe/session/sessionRequestFunctions.ts @@ -73,65 +73,13 @@ export async function getSessionFromRequest({ const sessionOptional = options?.sessionRequired === false; logDebugMessage("getSession: optional validation: " + sessionOptional); - const accessTokens: { - [key in TokenTransferMethod]?: ParsedJWTInfo; - } = {}; - - // We check all token transfer methods for available access tokens - for (const transferMethod of availableTokenTransferMethods) { - const tokenString = getToken(req, "access", transferMethod); - if (tokenString !== undefined) { - try { - const info = parseJWTWithoutSignatureVerification(tokenString); - validateAccessTokenStructure(info.payload, info.version); - logDebugMessage("getSession: got access token from " + transferMethod); - accessTokens[transferMethod] = info; - } catch { - logDebugMessage( - `getSession: ignoring token in ${transferMethod}, because it doesn't match our access token structure` - ); - } - } - } - const allowedTransferMethod = config.getTokenTransferMethod({ req, forCreateNewSession: false, userContext, }); - let requestTransferMethod: TokenTransferMethod | undefined; - let accessToken: ParsedJWTInfo | undefined; - if ( - (allowedTransferMethod === "any" || allowedTransferMethod === "header") && - accessTokens["header"] !== undefined - ) { - logDebugMessage("getSession: using header transfer method"); - requestTransferMethod = "header"; - accessToken = accessTokens["header"]; - } else if ( - (allowedTransferMethod === "any" || allowedTransferMethod === "cookie") && - accessTokens["cookie"] !== undefined - ) { - logDebugMessage("getSession: using cookie transfer method"); - - // If multiple access tokens exist in the request cookie, throw TRY_REFRESH_TOKEN. - // This prompts the client to call the refresh endpoint, clearing olderCookieDomain cookies (if set). - // ensuring outdated token payload isn't used. - const hasMultipleAccessTokenCookies = hasMultipleCookiesForTokenType(req, "access"); - if (hasMultipleAccessTokenCookies) { - logDebugMessage( - "getSession: Throwing TRY_REFRESH_TOKEN because multiple access tokens are present in request cookies" - ); - throw new SessionError({ - message: "Multiple access tokens present in the request cookies.", - type: SessionError.TRY_REFRESH_TOKEN, - }); - } - - requestTransferMethod = "cookie"; - accessToken = accessTokens["cookie"]; - } + const { requestTransferMethod, accessToken } = getAccessTokenFromRequest(req, allowedTransferMethod); let antiCsrfToken = getAntiCsrfTokenFromHeaders(req); let doAntiCsrfCheck = options !== undefined ? options.antiCsrfCheck : undefined; @@ -212,6 +160,64 @@ export async function getSessionFromRequest({ return session; } +export function getAccessTokenFromRequest(req: any, allowedTransferMethod: TokenTransferMethod | "any") { + const accessTokens: { + [key in TokenTransferMethod]?: ParsedJWTInfo; + } = {}; + + // We check all token transfer methods for available access tokens + for (const transferMethod of availableTokenTransferMethods) { + const tokenString = getToken(req, "access", transferMethod); + if (tokenString !== undefined) { + try { + const info = parseJWTWithoutSignatureVerification(tokenString); + validateAccessTokenStructure(info.payload, info.version); + logDebugMessage("getSession: got access token from " + transferMethod); + accessTokens[transferMethod] = info; + } catch { + logDebugMessage( + `getSession: ignoring token in ${transferMethod}, because it doesn't match our access token structure` + ); + } + } + } + + let requestTransferMethod: TokenTransferMethod | undefined; + let accessToken: ParsedJWTInfo | undefined; + + if ( + (allowedTransferMethod === "any" || allowedTransferMethod === "header") && + accessTokens["header"] !== undefined + ) { + logDebugMessage("getSession: using header transfer method"); + requestTransferMethod = "header"; + accessToken = accessTokens["header"]; + } else if ( + (allowedTransferMethod === "any" || allowedTransferMethod === "cookie") && + accessTokens["cookie"] !== undefined + ) { + logDebugMessage("getSession: using cookie transfer method"); + + // If multiple access tokens exist in the request cookie, throw TRY_REFRESH_TOKEN. + // This prompts the client to call the refresh endpoint, clearing olderCookieDomain cookies (if set). + // ensuring outdated token payload isn't used. + const hasMultipleAccessTokenCookies = hasMultipleCookiesForTokenType(req, "access"); + if (hasMultipleAccessTokenCookies) { + logDebugMessage( + "getSession: Throwing TRY_REFRESH_TOKEN because multiple access tokens are present in request cookies" + ); + throw new SessionError({ + message: "Multiple access tokens present in the request cookies.", + type: SessionError.TRY_REFRESH_TOKEN, + }); + } + + requestTransferMethod = "cookie"; + accessToken = accessTokens["cookie"]; + } + return { requestTransferMethod, accessToken, allowedTransferMethod }; +} + /* In all cases: if sIdRefreshToken token exists (so it's a legacy session) we clear it. Check http://localhost:3002/docs/contribute/decisions/session/0008 for further details and a table of expected behaviours diff --git a/lib/ts/recipe/session/utils.ts b/lib/ts/recipe/session/utils.ts index f43570f77..336152352 100644 --- a/lib/ts/recipe/session/utils.ts +++ b/lib/ts/recipe/session/utils.ts @@ -303,7 +303,7 @@ export function validateAndNormaliseUserInput( antiCsrfFunctionOrString: antiCsrf, override, invalidClaimStatusCode, - overwriteSessionDuringSignInUp: config?.overwriteSessionDuringSignInUp ?? false, + overwriteSessionDuringSignInUp: config?.overwriteSessionDuringSignInUp ?? true, jwksRefreshIntervalSec: config?.jwksRefreshIntervalSec ?? 3600 * 4, }; } diff --git a/lib/ts/recipe/thirdparty/api/implementation.ts b/lib/ts/recipe/thirdparty/api/implementation.ts index e91bb2dc3..889ba3685 100644 --- a/lib/ts/recipe/thirdparty/api/implementation.ts +++ b/lib/ts/recipe/thirdparty/api/implementation.ts @@ -137,6 +137,7 @@ export default function getAPIInterface(): APIInterface { tenantId: input.tenantId, userContext: input.userContext, session: input.session, + shouldTryLinkingWithSessionUser: input.shouldTryLinkingWithSessionUser, }); if (preAuthChecks.status !== "OK") { @@ -159,6 +160,7 @@ export default function getAPIInterface(): APIInterface { oAuthTokens: oAuthTokensToUse, rawUserInfoFromProvider: userInfo.rawUserInfoFromProvider, session: input.session, + shouldTryLinkingWithSessionUser: input.shouldTryLinkingWithSessionUser, tenantId, userContext, }); diff --git a/lib/ts/recipe/thirdparty/api/signinup.ts b/lib/ts/recipe/thirdparty/api/signinup.ts index f50813fe8..cc57dff1d 100644 --- a/lib/ts/recipe/thirdparty/api/signinup.ts +++ b/lib/ts/recipe/thirdparty/api/signinup.ts @@ -14,10 +14,14 @@ */ import STError from "../error"; -import { getBackwardsCompatibleUserInfo, send200Response } from "../../../utils"; +import { + getBackwardsCompatibleUserInfo, + getNormalisedShouldTryLinkingWithSessionUserFlag, + send200Response, +} from "../../../utils"; import { APIInterface, APIOptions } from "../"; import { UserContext } from "../../../types"; -import Session from "../../session"; +import { AuthUtils } from "../../../authUtils"; export default async function signInUpAPI( apiImplementation: APIInterface, @@ -82,13 +86,12 @@ export default async function signInUpAPI( const provider = providerResponse; - let session = await Session.getSession( + const shouldTryLinkingWithSessionUser = getNormalisedShouldTryLinkingWithSessionUserFlag(options.req, bodyParams); + + const session = await AuthUtils.loadSessionInAuthAPIIfNeeded( options.req, options.res, - { - sessionRequired: false, - overrideGlobalClaimValidators: () => [], - }, + shouldTryLinkingWithSessionUser, userContext ); @@ -102,6 +105,7 @@ export default async function signInUpAPI( oAuthTokens, tenantId, session, + shouldTryLinkingWithSessionUser, options, userContext, }); diff --git a/lib/ts/recipe/thirdparty/index.ts b/lib/ts/recipe/thirdparty/index.ts index 2dc3566f0..697bf96aa 100644 --- a/lib/ts/recipe/thirdparty/index.ts +++ b/lib/ts/recipe/thirdparty/index.ts @@ -136,6 +136,7 @@ export default class Wrapper { tenantId: tenantId === undefined ? DEFAULT_TENANT_ID : tenantId, isVerified, session, + shouldTryLinkingWithSessionUser: !!session, userContext: getUserContext(userContext), }); } diff --git a/lib/ts/recipe/thirdparty/providers/bitbucket.ts b/lib/ts/recipe/thirdparty/providers/bitbucket.ts index 56cc8073f..6d5ace5e5 100644 --- a/lib/ts/recipe/thirdparty/providers/bitbucket.ts +++ b/lib/ts/recipe/thirdparty/providers/bitbucket.ts @@ -14,7 +14,7 @@ */ import { ProviderInput, TypeProvider } from "../types"; -import { doGetRequest } from "./utils"; +import { doGetRequest } from "../../../thirdpartyUtils"; import NewProvider from "./custom"; import { logDebugMessage } from "../../../logger"; diff --git a/lib/ts/recipe/thirdparty/providers/custom.ts b/lib/ts/recipe/thirdparty/providers/custom.ts index 6e24fc0b6..ba7226836 100644 --- a/lib/ts/recipe/thirdparty/providers/custom.ts +++ b/lib/ts/recipe/thirdparty/providers/custom.ts @@ -1,5 +1,5 @@ import { TypeProvider, ProviderInput, UserInfo, ProviderConfigForClientType } from "../types"; -import { doGetRequest, doPostRequest, verifyIdTokenFromJWKSEndpointAndGetPayload } from "./utils"; +import { doGetRequest, doPostRequest, verifyIdTokenFromJWKSEndpointAndGetPayload } from "../../../thirdpartyUtils"; import pkceChallenge from "pkce-challenge"; import { getProviderConfigForClient } from "./configUtils"; import { JWTVerifyGetKey, createRemoteJWKSet } from "jose"; diff --git a/lib/ts/recipe/thirdparty/providers/github.ts b/lib/ts/recipe/thirdparty/providers/github.ts index f556eba71..e1f218143 100644 --- a/lib/ts/recipe/thirdparty/providers/github.ts +++ b/lib/ts/recipe/thirdparty/providers/github.ts @@ -14,7 +14,7 @@ */ import { ProviderInput, TypeProvider, UserInfo } from "../types"; import NewProvider from "./custom"; -import { doGetRequest, doPostRequest } from "./utils"; +import { doGetRequest, doPostRequest } from "../../../thirdpartyUtils"; function getSupertokensUserInfoFromRawUserInfoResponseForGithub(rawUserInfoResponse: { fromIdTokenPayload?: any; diff --git a/lib/ts/recipe/thirdparty/providers/linkedin.ts b/lib/ts/recipe/thirdparty/providers/linkedin.ts index 5aa79976f..c179b0269 100644 --- a/lib/ts/recipe/thirdparty/providers/linkedin.ts +++ b/lib/ts/recipe/thirdparty/providers/linkedin.ts @@ -15,7 +15,7 @@ import { logDebugMessage } from "../../../logger"; import { ProviderInput, TypeProvider } from "../types"; import NewProvider from "./custom"; -import { doGetRequest } from "./utils"; +import { doGetRequest } from "../../../thirdpartyUtils"; export default function Linkedin(input: ProviderInput): TypeProvider { if (input.config.name === undefined) { diff --git a/lib/ts/recipe/thirdparty/providers/twitter.ts b/lib/ts/recipe/thirdparty/providers/twitter.ts index cb60db8d3..083cf821f 100644 --- a/lib/ts/recipe/thirdparty/providers/twitter.ts +++ b/lib/ts/recipe/thirdparty/providers/twitter.ts @@ -19,7 +19,7 @@ import NewProvider, { getActualClientIdFromDevelopmentClientId, isUsingDevelopmentClientId, } from "./custom"; -import { doPostRequest } from "./utils"; +import { doPostRequest } from "../../../thirdpartyUtils"; export default function Twitter(input: ProviderInput): TypeProvider { if (input.config.name === undefined) { diff --git a/lib/ts/recipe/thirdparty/providers/utils.ts b/lib/ts/recipe/thirdparty/providers/utils.ts index 0b6a2244e..a82280d32 100644 --- a/lib/ts/recipe/thirdparty/providers/utils.ts +++ b/lib/ts/recipe/thirdparty/providers/utils.ts @@ -1,122 +1,7 @@ -import * as jose from "jose"; - import { ProviderConfigForClientType } from "../types"; +import { getOIDCDiscoveryInfo } from "../../../thirdpartyUtils"; import NormalisedURLDomain from "../../../normalisedURLDomain"; import NormalisedURLPath from "../../../normalisedURLPath"; -import { logDebugMessage } from "../../../logger"; -import { doFetch } from "../../../utils"; - -export async function doGetRequest( - url: string, - queryParams?: { [key: string]: string }, - headers?: { [key: string]: string } -): Promise<{ - jsonResponse: Record | undefined; - status: number; - stringResponse: string; -}> { - logDebugMessage( - `GET request to ${url}, with query params ${JSON.stringify(queryParams)} and headers ${JSON.stringify(headers)}` - ); - if (headers?.["Accept"] === undefined) { - headers = { - ...headers, - Accept: "application/json", // few providers like github don't send back json response by default - }; - } - const finalURL = new URL(url); - finalURL.search = new URLSearchParams(queryParams).toString(); - let response = await doFetch(finalURL.toString(), { - headers: headers, - }); - - const stringResponse = await response.text(); - let jsonResponse: Record | undefined = undefined; - - if (response.status < 400) { - jsonResponse = JSON.parse(stringResponse); - } - - logDebugMessage(`Received response with status ${response.status} and body ${stringResponse}`); - return { - stringResponse, - status: response.status, - jsonResponse, - }; -} - -export async function doPostRequest( - url: string, - params: { [key: string]: any }, - headers?: { [key: string]: string } -): Promise<{ - jsonResponse: Record | undefined; - status: number; - stringResponse: string; -}> { - if (headers === undefined) { - headers = {}; - } - - headers["Content-Type"] = "application/x-www-form-urlencoded"; - headers["Accept"] = "application/json"; // few providers like github don't send back json response by default - - logDebugMessage( - `POST request to ${url}, with params ${JSON.stringify(params)} and headers ${JSON.stringify(headers)}` - ); - - const body = new URLSearchParams(params).toString(); - let response = await doFetch(url, { - method: "POST", - body, - headers, - }); - - const stringResponse = await response.text(); - let jsonResponse: Record | undefined = undefined; - - if (response.status < 400) { - jsonResponse = JSON.parse(stringResponse); - } - - logDebugMessage(`Received response with status ${response.status} and body ${stringResponse}`); - return { - stringResponse, - status: response.status, - jsonResponse, - }; -} - -export async function verifyIdTokenFromJWKSEndpointAndGetPayload( - idToken: string, - jwks: jose.JWTVerifyGetKey, - otherOptions: jose.JWTVerifyOptions -): Promise { - const { payload } = await jose.jwtVerify(idToken, jwks, otherOptions); - - return payload; -} - -// OIDC utils -var oidcInfoMap: { [key: string]: any } = {}; - -async function getOIDCDiscoveryInfo(issuer: string): Promise { - if (oidcInfoMap[issuer] !== undefined) { - return oidcInfoMap[issuer]; - } - - const normalizedDomain = new NormalisedURLDomain(issuer); - const normalizedPath = new NormalisedURLPath(issuer); - - let oidcInfo = await doGetRequest(normalizedDomain.getAsStringDangerous() + normalizedPath.getAsStringDangerous()); - if (oidcInfo.status > 400) { - logDebugMessage(`Received response with status ${oidcInfo.status} and body ${oidcInfo.stringResponse}`); - throw new Error(`Received response with status ${oidcInfo!.status} and body ${oidcInfo!.stringResponse}`); - } - - oidcInfoMap[issuer] = oidcInfo.jsonResponse!; - return oidcInfo.jsonResponse!; -} export async function discoverOIDCEndpoints(config: ProviderConfigForClientType): Promise { if (config.oidcDiscoveryEndpoint !== undefined) { diff --git a/lib/ts/recipe/thirdparty/recipeImplementation.ts b/lib/ts/recipe/thirdparty/recipeImplementation.ts index f9a602e3c..6225009bd 100644 --- a/lib/ts/recipe/thirdparty/recipeImplementation.ts +++ b/lib/ts/recipe/thirdparty/recipeImplementation.ts @@ -15,7 +15,16 @@ export default function getRecipeImplementation(querier: Querier, providers: Pro return { manuallyCreateOrUpdateUser: async function ( this: RecipeInterface, - { thirdPartyId, thirdPartyUserId, email, isVerified, tenantId, session, userContext } + { + thirdPartyId, + thirdPartyUserId, + email, + isVerified, + tenantId, + session, + shouldTryLinkingWithSessionUser, + userContext, + } ) { const accountLinking = AccountLinking.getInstance(); const users = await listUsersByAccountInfo( @@ -71,8 +80,9 @@ export default function getRecipeImplementation(querier: Querier, providers: Pro // function updated the verification status) and can return that response.user = (await getUser(response.recipeUserId.getAsString(), userContext))!; - const linkResult = await AuthUtils.linkToSessionIfProvidedElseCreatePrimaryUserIdOrLinkByAccountInfo({ + const linkResult = await AuthUtils.linkToSessionIfRequiredElseCreatePrimaryUserIdOrLinkByAccountInfo({ tenantId, + shouldTryLinkingWithSessionUser, inputUser: response.user, recipeUserId: response.recipeUserId, session, @@ -102,6 +112,7 @@ export default function getRecipeImplementation(querier: Querier, providers: Pro userContext, oAuthTokens, session, + shouldTryLinkingWithSessionUser, rawUserInfoFromProvider, } ): Promise< @@ -136,6 +147,7 @@ export default function getRecipeImplementation(querier: Querier, providers: Pro tenantId, isVerified, session, + shouldTryLinkingWithSessionUser, userContext, }); diff --git a/lib/ts/recipe/thirdparty/types.ts b/lib/ts/recipe/thirdparty/types.ts index 21c7608da..71e9847d3 100644 --- a/lib/ts/recipe/thirdparty/types.ts +++ b/lib/ts/recipe/thirdparty/types.ts @@ -174,6 +174,7 @@ export type RecipeInterface = { fromUserInfoAPI?: { [key: string]: any }; }; session: SessionContainerInterface | undefined; + shouldTryLinkingWithSessionUser: boolean | undefined; tenantId: string; userContext: UserContext; }): Promise< @@ -208,6 +209,7 @@ export type RecipeInterface = { email: string; isVerified: boolean; session: SessionContainerInterface | undefined; + shouldTryLinkingWithSessionUser: boolean | undefined; tenantId: string; userContext: UserContext; }): Promise< @@ -272,6 +274,7 @@ export type APIInterface = { provider: TypeProvider; tenantId: string; session: SessionContainerInterface | undefined; + shouldTryLinkingWithSessionUser: boolean | undefined; options: APIOptions; userContext: UserContext; } & ( diff --git a/lib/ts/recipe/totp/recipeImplementation.ts b/lib/ts/recipe/totp/recipeImplementation.ts index 745f23cf2..a87a35c39 100644 --- a/lib/ts/recipe/totp/recipeImplementation.ts +++ b/lib/ts/recipe/totp/recipeImplementation.ts @@ -123,6 +123,7 @@ export default function getRecipeInterface(querier: Querier, config: TypeNormali existingDeviceName: input.existingDeviceName, newDeviceName: input.newDeviceName, }, + {}, input.userContext ); }, diff --git a/lib/ts/recipe/usermetadata/recipeImplementation.ts b/lib/ts/recipe/usermetadata/recipeImplementation.ts index 51ab77ea5..2ab9d24e2 100644 --- a/lib/ts/recipe/usermetadata/recipeImplementation.ts +++ b/lib/ts/recipe/usermetadata/recipeImplementation.ts @@ -30,6 +30,7 @@ export default function getRecipeInterface(querier: Querier): RecipeInterface { userId, metadataUpdate, }, + {}, userContext ); }, diff --git a/lib/ts/recipe/userroles/recipe.ts b/lib/ts/recipe/userroles/recipe.ts index 0f0176c7b..36befb4f2 100644 --- a/lib/ts/recipe/userroles/recipe.ts +++ b/lib/ts/recipe/userroles/recipe.ts @@ -19,7 +19,7 @@ import type { BaseRequest, BaseResponse } from "../../framework"; 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 RecipeImplementation from "./recipeImplementation"; import { RecipeInterface, TypeInput, TypeNormalisedInput } from "./types"; @@ -27,8 +27,11 @@ import { validateAndNormaliseUserInput } from "./utils"; import OverrideableBuilder from "supertokens-js-override"; import { PostSuperTokensInitCallbacks } from "../../postSuperTokensInitCallbacks"; import SessionRecipe from "../session/recipe"; +import OAuth2Recipe from "../oauth2provider/recipe"; import { UserRoleClaim } from "./userRoleClaim"; import { PermissionClaim } from "./permissionClaim"; +import { User } from "../../user"; +import { getSessionInformation } from "../session"; export default class Recipe extends RecipeModule { static RECIPE_ID = "userroles"; @@ -55,6 +58,114 @@ export default class Recipe extends RecipeModule { if (!this.config.skipAddingPermissionsToAccessToken) { SessionRecipe.getInstanceOrThrowError().addClaimFromOtherRecipe(PermissionClaim); } + + const tokenPayloadBuilder = async ( + user: User, + scopes: string[], + sessionHandle: string, + userContext: UserContext + ) => { + let payload: { + roles?: string[]; + permissions?: string[]; + } = {}; + + const sessionInfo = await getSessionInformation(sessionHandle, userContext); + + let userRoles: string[] = []; + + if (scopes.includes("roles") || scopes.includes("permissions")) { + const res = await this.recipeInterfaceImpl.getRolesForUser({ + userId: user.id, + tenantId: sessionInfo!.tenantId, + userContext, + }); + + if (res.status !== "OK") { + throw new Error("Failed to fetch roles for the user"); + } + userRoles = res.roles; + } + + if (scopes.includes("roles")) { + payload.roles = userRoles; + } + + if (scopes.includes("permissions")) { + const userPermissions = new Set(); + for (const role of userRoles) { + const rolePermissions = await this.recipeInterfaceImpl.getPermissionsForRole({ + role, + userContext, + }); + + if (rolePermissions.status !== "OK") { + throw new Error("Failed to fetch permissions for the role"); + } + + for (const perm of rolePermissions.permissions) { + userPermissions.add(perm); + } + } + + payload.permissions = Array.from(userPermissions); + } + + return payload; + }; + + OAuth2Recipe.getInstanceOrThrowError().addAccessTokenBuilderFromOtherRecipe(tokenPayloadBuilder); + OAuth2Recipe.getInstanceOrThrowError().addIdTokenBuilderFromOtherRecipe(tokenPayloadBuilder); + + OAuth2Recipe.getInstanceOrThrowError().addUserInfoBuilderFromOtherRecipe( + async (user, _accessTokenPayload, scopes, tenantId, userContext) => { + let userInfo: { + roles?: string[]; + permissions?: string[]; + } = {}; + + let userRoles: string[] = []; + + if (scopes.includes("roles") || scopes.includes("permissions")) { + const res = await this.recipeInterfaceImpl.getRolesForUser({ + userId: user.id, + tenantId, + userContext, + }); + + if (res.status !== "OK") { + throw new Error("Failed to fetch roles for the user"); + } + userRoles = res.roles; + } + + if (scopes.includes("roles")) { + userInfo.roles = userRoles; + } + + if (scopes.includes("permissions")) { + const userPermissions = new Set(); + for (const role of userRoles) { + const rolePermissions = await this.recipeInterfaceImpl.getPermissionsForRole({ + role, + userContext, + }); + + if (rolePermissions.status !== "OK") { + throw new Error("Failed to fetch permissions for the role"); + } + + for (const perm of rolePermissions.permissions) { + userPermissions.add(perm); + } + } + + userInfo.permissions = Array.from(userPermissions); + } + + return userInfo; + } + ); }); } diff --git a/lib/ts/recipe/userroles/recipeImplementation.ts b/lib/ts/recipe/userroles/recipeImplementation.ts index eaee083f7..7141a8bfe 100644 --- a/lib/ts/recipe/userroles/recipeImplementation.ts +++ b/lib/ts/recipe/userroles/recipeImplementation.ts @@ -24,6 +24,7 @@ export default function getRecipeInterface(querier: Querier): RecipeInterface { return querier.sendPutRequest( new NormalisedURLPath(`/${tenantId === undefined ? DEFAULT_TENANT_ID : tenantId}/recipe/user/role`), { userId, role }, + {}, userContext ); }, @@ -55,7 +56,12 @@ export default function getRecipeInterface(querier: Querier): RecipeInterface { }, createNewRoleOrAddPermissions: function ({ role, permissions, userContext }) { - return querier.sendPutRequest(new NormalisedURLPath("/recipe/role"), { role, permissions }, userContext); + return querier.sendPutRequest( + new NormalisedURLPath("/recipe/role"), + { role, permissions }, + {}, + userContext + ); }, getPermissionsForRole: function ({ role, userContext }) { diff --git a/lib/ts/recipeModule.ts b/lib/ts/recipeModule.ts index 3b73692c8..1f3c14b45 100644 --- a/lib/ts/recipeModule.ts +++ b/lib/ts/recipeModule.ts @@ -22,7 +22,7 @@ import { DEFAULT_TENANT_ID } from "./recipe/multitenancy/constants"; export default abstract class RecipeModule { private recipeId: string; - private appInfo: NormalisedAppinfo; + protected appInfo: NormalisedAppinfo; constructor(recipeId: string, appInfo: NormalisedAppinfo) { this.recipeId = recipeId; @@ -41,7 +41,7 @@ export default abstract class RecipeModule { path: NormalisedURLPath, method: HTTPMethod, userContext: UserContext - ): Promise<{ id: string; tenantId: string } | undefined> => { + ): Promise<{ id: string; tenantId: string; exactMatch: boolean } | undefined> => { let apisHandled = this.getAPIsHandled(); const basePathStr = this.appInfo.apiBasePath.getAsStringDangerous(); @@ -71,7 +71,7 @@ export default abstract class RecipeModule { tenantIdFromFrontend: DEFAULT_TENANT_ID, userContext, }); - return { id: currAPI.id, tenantId: finalTenantId }; + return { id: currAPI.id, tenantId: finalTenantId, exactMatch: true }; } else if ( remainingPath !== undefined && this.appInfo.apiBasePath @@ -82,7 +82,7 @@ export default abstract class RecipeModule { tenantIdFromFrontend: tenantId === undefined ? DEFAULT_TENANT_ID : tenantId, userContext, }); - return { id: currAPI.id, tenantId: finalTenantId }; + return { id: currAPI.id, tenantId: finalTenantId, exactMatch: false }; } } } diff --git a/lib/ts/supertokens.ts b/lib/ts/supertokens.ts index 78109d1d0..9f5df3e0b 100644 --- a/lib/ts/supertokens.ts +++ b/lib/ts/supertokens.ts @@ -103,6 +103,7 @@ export default class SuperTokens { let totpFound = false; let userMetadataFound = false; let multiFactorAuthFound = false; + let oauth2Found = false; // Multitenancy recipe is an always initialized recipe and needs to be imported this way // so that there is no circular dependency. Otherwise there would be cyclic dependency @@ -111,6 +112,7 @@ export default class SuperTokens { let UserMetadataRecipe = require("./recipe/usermetadata/recipe").default; let MultiFactorAuthRecipe = require("./recipe/multifactorauth/recipe").default; let TotpRecipe = require("./recipe/totp/recipe").default; + let OAuth2ProviderRecipe = require("./recipe/oauth2provider/recipe").default; this.recipeModules = config.recipeList.map((func) => { const recipeModule = func(this.appInfo, this.isInServerlessEnv); @@ -122,6 +124,8 @@ export default class SuperTokens { multiFactorAuthFound = true; } else if (recipeModule.getRecipeId() === TotpRecipe.RECIPE_ID) { totpFound = true; + } else if (recipeModule.getRecipeId() === OAuth2ProviderRecipe.RECIPE_ID) { + oauth2Found = true; } return recipeModule; }); @@ -142,6 +146,10 @@ export default class SuperTokens { // To let those cases function without initializing account linking we do not check it here, but when // the authentication endpoints are called. + // We've decided to always initialize the OAuth2Provider recipe + if (!oauth2Found) { + this.recipeModules.push(OAuth2ProviderRecipe.init()(this.appInfo, this.isInServerlessEnv)); + } this.telemetryEnabled = config.telemetry === undefined ? process.env.TEST_MODE !== "testing" : config.telemetry; } @@ -333,6 +341,7 @@ export default class SuperTokens { userIdType: input.userIdType, externalUserIdInfo: input.externalUserIdInfo, }, + {}, input.userContext ); } else { @@ -362,6 +371,13 @@ export default class SuperTokens { } async function handleWithoutRid(recipeModules: RecipeModule[]) { + let bestMatch: + | { + recipeModule: RecipeModule; + idResult: { id: string; tenantId: string; exactMatch: boolean }; + } + | undefined = undefined; + for (let i = 0; i < recipeModules.length; i++) { logDebugMessage( "middleware: Checking recipe ID for match: " + @@ -373,24 +389,40 @@ export default class SuperTokens { ); let idResult = await recipeModules[i].returnAPIIdIfCanHandleRequest(path, method, userContext); if (idResult !== undefined) { - logDebugMessage("middleware: Request being handled by recipe. ID is: " + idResult.id); - let requestHandled = await recipeModules[i].handleAPIRequest( - idResult.id, - idResult.tenantId, - request, - response, - path, - method, - userContext - ); - if (!requestHandled) { - logDebugMessage("middleware: Not handled because API returned requestHandled as false"); - return false; + // The request path may or may not include the tenantId. `returnAPIIdIfCanHandleRequest` handles both cases. + // If one recipe matches with tenantId and another matches exactly, we prefer the exact match. + if (bestMatch === undefined || idResult.exactMatch) { + bestMatch = { + recipeModule: recipeModules[i], + idResult: idResult, + }; + } + + if (idResult.exactMatch) { + break; } - logDebugMessage("middleware: Ended"); - return true; } } + + if (bestMatch !== undefined) { + const { idResult, recipeModule } = bestMatch; + logDebugMessage("middleware: Request being handled by recipe. ID is: " + idResult.id); + let requestHandled = await recipeModule.handleAPIRequest( + idResult.id, + idResult.tenantId, + request, + response, + path, + method, + userContext + ); + if (!requestHandled) { + logDebugMessage("middleware: Not handled because API returned requestHandled as false"); + return false; + } + logDebugMessage("middleware: Ended"); + return true; + } logDebugMessage("middleware: Not handling because no recipe matched"); return false; } @@ -434,6 +466,7 @@ export default class SuperTokens { | { id: string; tenantId: string; + exactMatch: boolean; } | undefined = undefined; @@ -443,13 +476,18 @@ export default class SuperTokens { // the path and methods of the APIs exposed via those recipes is unique. let currIdResult = await matchedRecipe[i].returnAPIIdIfCanHandleRequest(path, method, userContext); if (currIdResult !== undefined) { - if (idResult !== undefined) { + if ( + idResult === undefined || + // The request path may or may not include the tenantId. `returnAPIIdIfCanHandleRequest` handles both cases. + // If one recipe matches with tenantId and another matches exactly, we prefer the exact match. + (currIdResult.exactMatch === true && idResult.exactMatch === false) + ) { + finalMatchedRecipe = matchedRecipe[i]; + idResult = currIdResult; + } else { throw new Error( "Two recipes have matched the same API path and method! This is a bug in the SDK. Please contact support." ); - } else { - finalMatchedRecipe = matchedRecipe[i]; - idResult = currIdResult; } } } diff --git a/lib/ts/thirdpartyUtils.ts b/lib/ts/thirdpartyUtils.ts new file mode 100644 index 000000000..54a268888 --- /dev/null +++ b/lib/ts/thirdpartyUtils.ts @@ -0,0 +1,119 @@ +import * as jose from "jose"; +import { logDebugMessage } from "./logger"; +import { doFetch } from "./utils"; +import NormalisedURLDomain from "./normalisedURLDomain"; +import NormalisedURLPath from "./normalisedURLPath"; + +export async function doGetRequest( + url: string, + queryParams?: { [key: string]: string }, + headers?: { [key: string]: string } +): Promise<{ + jsonResponse: Record | undefined; + status: number; + stringResponse: string; +}> { + logDebugMessage( + `GET request to ${url}, with query params ${JSON.stringify(queryParams)} and headers ${JSON.stringify(headers)}` + ); + if (headers?.["Accept"] === undefined) { + headers = { + ...headers, + Accept: "application/json", + }; + } + const finalURL = new URL(url); + finalURL.search = new URLSearchParams(queryParams).toString(); + let response = await doFetch(finalURL.toString(), { + headers: headers, + }); + + const stringResponse = await response.text(); + let jsonResponse: Record | undefined = undefined; + + if (response.status < 400) { + jsonResponse = JSON.parse(stringResponse); + } + + logDebugMessage(`Received response with status ${response.status} and body ${stringResponse}`); + return { + stringResponse, + status: response.status, + jsonResponse, + }; +} + +export async function doPostRequest( + url: string, + params: { [key: string]: any }, + headers?: { [key: string]: string } +): Promise<{ + jsonResponse: Record | undefined; + status: number; + stringResponse: string; +}> { + if (headers === undefined) { + headers = {}; + } + + headers["Content-Type"] = "application/x-www-form-urlencoded"; + headers["Accept"] = "application/json"; + + logDebugMessage( + `POST request to ${url}, with params ${JSON.stringify(params)} and headers ${JSON.stringify(headers)}` + ); + + const body = new URLSearchParams(params).toString(); + let response = await doFetch(url, { + method: "POST", + body, + headers, + }); + + const stringResponse = await response.text(); + let jsonResponse: Record | undefined = undefined; + + if (response.status < 400) { + jsonResponse = JSON.parse(stringResponse); + } + + logDebugMessage(`Received response with status ${response.status} and body ${stringResponse}`); + return { + stringResponse, + status: response.status, + jsonResponse, + }; +} + +export async function verifyIdTokenFromJWKSEndpointAndGetPayload( + idToken: string, + jwks: jose.JWTVerifyGetKey, + otherOptions: jose.JWTVerifyOptions +): Promise { + const { payload } = await jose.jwtVerify(idToken, jwks, otherOptions); + + return payload; +} + +// OIDC utils +var oidcInfoMap: { [key: string]: any } = {}; + +export async function getOIDCDiscoveryInfo(issuer: string): Promise { + const normalizedDomain = new NormalisedURLDomain(issuer); + let normalizedPath = new NormalisedURLPath(issuer); + + if (oidcInfoMap[issuer] !== undefined) { + return oidcInfoMap[issuer]; + } + const oidcInfo = await doGetRequest( + normalizedDomain.getAsStringDangerous() + normalizedPath.getAsStringDangerous() + ); + + if (oidcInfo.status >= 400) { + logDebugMessage(`Received response with status ${oidcInfo.status} and body ${oidcInfo.stringResponse}`); + throw new Error(`Received response with status ${oidcInfo.status} and body ${oidcInfo.stringResponse}`); + } + + oidcInfoMap[issuer] = oidcInfo.jsonResponse!; + return oidcInfo.jsonResponse!; +} diff --git a/lib/ts/types.ts b/lib/ts/types.ts index a90d96f0f..e87e6b2a7 100644 --- a/lib/ts/types.ts +++ b/lib/ts/types.ts @@ -24,6 +24,11 @@ type Brand = { [__brand]: B }; type Branded = T & Brand; +// A utility type that makes all properties of a given type non-nullable. +export type NonNullableProperties = { + [P in keyof T]: NonNullable; +}; + // Record is still quite generic and we would like to ensure type safety for the userContext // so we use the concept of branded type, which enables catching of issues at compile time. // Detailed explanation about branded types is available here - https://egghead.io/blog/using-branded-types-in-typescript @@ -86,7 +91,7 @@ export type APIHandled = { disabled: boolean; }; -export type HTTPMethod = "post" | "get" | "delete" | "put" | "options" | "trace"; +export type HTTPMethod = "post" | "get" | "delete" | "put" | "patch" | "options" | "trace"; export type JSONPrimitive = string | number | boolean | null; export type JSONArray = Array; diff --git a/lib/ts/utils.ts b/lib/ts/utils.ts index b09011bf9..144fd04a9 100644 --- a/lib/ts/utils.ts +++ b/lib/ts/utils.ts @@ -18,11 +18,13 @@ export const doFetch: typeof fetch = async (input: RequestInfo | URL, init?: Req ProcessState.getInstance().addState(PROCESS_STATE.ADDING_NO_CACHE_HEADER_IN_FETCH); init = { cache: "no-cache", + redirect: "manual", }; } else { if (init.cache === undefined) { ProcessState.getInstance().addState(PROCESS_STATE.ADDING_NO_CACHE_HEADER_IN_FETCH); init.cache = "no-cache"; + init.redirect = "manual"; } } const fetchFunction = typeof fetch !== "undefined" ? fetch : crossFetch; @@ -192,6 +194,12 @@ export function isAnIpAddress(ipaddress: string) { ipaddress ); } +export function getNormalisedShouldTryLinkingWithSessionUserFlag(req: BaseRequest, body: any) { + if (hasGreaterThanEqualToFDI(req, "3.1")) { + return body.shouldTryLinkingWithSessionUser ?? false; + } + return undefined; +} export function getBackwardsCompatibleUserInfo( req: BaseRequest, @@ -422,3 +430,24 @@ export function normaliseEmail(email: string): string { return email; } + +export function toCamelCase(str: string): string { + return str.replace(/([-_][a-z])/gi, (match) => { + return match.toUpperCase().replace("-", "").replace("_", ""); + }); +} + +export function toSnakeCase(str: string): string { + return str.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`); +} + +// Transforms the keys of an object from camelCase to snakeCase or vice versa. +export function transformObjectKeys(obj: { [key: string]: any }, caseType: "snake-case" | "camelCase"): T { + const transformKey = caseType === "camelCase" ? toCamelCase : toSnakeCase; + + return Object.entries(obj).reduce((result, [key, value]) => { + const transformedKey = transformKey(key); + result[transformedKey] = value; + return result; + }, {} as any) as T; +} diff --git a/lib/ts/version.ts b/lib/ts/version.ts index f7a917b2c..22817ea60 100644 --- a/lib/ts/version.ts +++ b/lib/ts/version.ts @@ -12,7 +12,7 @@ * License for the specific language governing permissions and limitations * under the License. */ -export const version = "20.0.0"; +export const version = "20.0.2"; export const cdiSupported = ["5.1"]; diff --git a/package-lock.json b/package-lock.json index 967c8fbae..238781e13 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "supertokens-node", - "version": "20.0.0", + "version": "20.0.2", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "supertokens-node", - "version": "20.0.0", + "version": "20.0.2", "license": "Apache-2.0", "dependencies": { "content-type": "^1.0.5", @@ -19,6 +19,7 @@ "nodemailer": "^6.7.2", "pkce-challenge": "^3.0.0", "psl": "1.8.0", + "set-cookie-parser": "^2.6.0", "supertokens-js-override": "^0.0.4", "twilio": "^4.19.3" }, @@ -39,6 +40,7 @@ "@types/koa-bodyparser": "^4.3.3", "@types/nodemailer": "^6.4.4", "@types/psl": "1.1.0", + "@types/set-cookie-parser": "^2.4.9", "@types/validator": "10.11.0", "aws-sdk-mock": "^5.4.0", "body-parser": "1.20.1", @@ -1713,6 +1715,15 @@ "@types/node": "*" } }, + "node_modules/@types/set-cookie-parser": { + "version": "2.4.9", + "resolved": "https://registry.npmjs.org/@types/set-cookie-parser/-/set-cookie-parser-2.4.9.tgz", + "integrity": "sha512-bCorlULvl0xTdjj4BPUHX4cqs9I+go2TfW/7Do1nnFYWS0CPP429Qr1AY42kiFhCwLpvAkWFr1XIBHd8j6/MCQ==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/type-is": { "version": "1.6.6", "resolved": "https://registry.npmjs.org/@types/type-is/-/type-is-1.6.6.tgz", @@ -7010,8 +7021,7 @@ "node_modules/set-cookie-parser": { "version": "2.6.0", "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.6.0.tgz", - "integrity": "sha512-RVnVQxTXuerk653XfuliOxBP81Sf0+qfQE73LIYKcyMYHG94AuH0kgrQpRDuTZnSmjpysHmzxJXKNfa6PjFhyQ==", - "dev": true + "integrity": "sha512-RVnVQxTXuerk653XfuliOxBP81Sf0+qfQE73LIYKcyMYHG94AuH0kgrQpRDuTZnSmjpysHmzxJXKNfa6PjFhyQ==" }, "node_modules/set-function-length": { "version": "1.2.2", @@ -9568,6 +9578,15 @@ "@types/node": "*" } }, + "@types/set-cookie-parser": { + "version": "2.4.9", + "resolved": "https://registry.npmjs.org/@types/set-cookie-parser/-/set-cookie-parser-2.4.9.tgz", + "integrity": "sha512-bCorlULvl0xTdjj4BPUHX4cqs9I+go2TfW/7Do1nnFYWS0CPP429Qr1AY42kiFhCwLpvAkWFr1XIBHd8j6/MCQ==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, "@types/type-is": { "version": "1.6.6", "resolved": "https://registry.npmjs.org/@types/type-is/-/type-is-1.6.6.tgz", @@ -13642,8 +13661,7 @@ "set-cookie-parser": { "version": "2.6.0", "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.6.0.tgz", - "integrity": "sha512-RVnVQxTXuerk653XfuliOxBP81Sf0+qfQE73LIYKcyMYHG94AuH0kgrQpRDuTZnSmjpysHmzxJXKNfa6PjFhyQ==", - "dev": true + "integrity": "sha512-RVnVQxTXuerk653XfuliOxBP81Sf0+qfQE73LIYKcyMYHG94AuH0kgrQpRDuTZnSmjpysHmzxJXKNfa6PjFhyQ==" }, "set-function-length": { "version": "1.2.2", diff --git a/package.json b/package.json index ff2178e7e..fa879d3ec 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "supertokens-node", - "version": "20.0.0", + "version": "20.0.2", "description": "NodeJS driver for SuperTokens core", "main": "index.js", "scripts": { @@ -123,6 +123,7 @@ "nodemailer": "^6.7.2", "pkce-challenge": "^3.0.0", "psl": "1.8.0", + "set-cookie-parser": "^2.6.0", "supertokens-js-override": "^0.0.4", "twilio": "^4.19.3" }, @@ -143,6 +144,7 @@ "@types/koa-bodyparser": "^4.3.3", "@types/nodemailer": "^6.4.4", "@types/psl": "1.1.0", + "@types/set-cookie-parser": "^2.4.9", "@types/validator": "10.11.0", "aws-sdk-mock": "^5.4.0", "body-parser": "1.20.1", diff --git a/recipe/oauth2client/index.d.ts b/recipe/oauth2client/index.d.ts new file mode 100644 index 000000000..89f4241f8 --- /dev/null +++ b/recipe/oauth2client/index.d.ts @@ -0,0 +1,10 @@ +export * from "../../lib/build/recipe/oauth2client"; +/** + * 'export *' does not re-export a default. + * import NextJS from "supertokens-node/nextjs"; + * the above import statement won't be possible unless either + * - user add "esModuleInterop": true in their tsconfig.json file + * - we do the following change: + */ +import * as _default from "../../lib/build/recipe/oauth2client"; +export default _default; diff --git a/recipe/oauth2client/index.js b/recipe/oauth2client/index.js new file mode 100644 index 000000000..f1b31d6db --- /dev/null +++ b/recipe/oauth2client/index.js @@ -0,0 +1,6 @@ +"use strict"; +function __export(m) { + for (var p in m) if (!exports.hasOwnProperty(p)) exports[p] = m[p]; +} +exports.__esModule = true; +__export(require("../../lib/build/recipe/oauth2client")); diff --git a/recipe/oauth2client/types/index.d.ts b/recipe/oauth2client/types/index.d.ts new file mode 100644 index 000000000..e475d4576 --- /dev/null +++ b/recipe/oauth2client/types/index.d.ts @@ -0,0 +1,10 @@ +export * from "../../../lib/build/recipe/oauth2client/types"; +/** + * 'export *' does not re-export a default. + * import NextJS from "supertokens-node/nextjs"; + * the above import statement won't be possible unless either + * - user add "esModuleInterop": true in their tsconfig.json file + * - we do the following change: + */ +import * as _default from "../../../lib/build/recipe/oauth2client/types"; +export default _default; diff --git a/recipe/oauth2client/types/index.js b/recipe/oauth2client/types/index.js new file mode 100644 index 000000000..01b5c40c6 --- /dev/null +++ b/recipe/oauth2client/types/index.js @@ -0,0 +1,6 @@ +"use strict"; +function __export(m) { + for (var p in m) if (!exports.hasOwnProperty(p)) exports[p] = m[p]; +} +exports.__esModule = true; +__export(require("../../../lib/build/recipe/oauth2client/types")); diff --git a/recipe/oauth2provider/index.d.ts b/recipe/oauth2provider/index.d.ts new file mode 100644 index 000000000..72d8d7e7d --- /dev/null +++ b/recipe/oauth2provider/index.d.ts @@ -0,0 +1,10 @@ +export * from "../../lib/build/recipe/oauth2provider"; +/** + * 'export *' does not re-export a default. + * import NextJS from "supertokens-node/nextjs"; + * the above import statement won't be possible unless either + * - user add "esModuleInterop": true in their tsconfig.json file + * - we do the following change: + */ +import * as _default from "../../lib/build/recipe/oauth2provider"; +export default _default; diff --git a/recipe/oauth2provider/index.js b/recipe/oauth2provider/index.js new file mode 100644 index 000000000..1b9fa1030 --- /dev/null +++ b/recipe/oauth2provider/index.js @@ -0,0 +1,6 @@ +"use strict"; +function __export(m) { + for (var p in m) if (!exports.hasOwnProperty(p)) exports[p] = m[p]; +} +exports.__esModule = true; +__export(require("../../lib/build/recipe/oauth2provider")); diff --git a/recipe/oauth2provider/types/index.d.ts b/recipe/oauth2provider/types/index.d.ts new file mode 100644 index 000000000..bdfe9d82c --- /dev/null +++ b/recipe/oauth2provider/types/index.d.ts @@ -0,0 +1,10 @@ +export * from "../../../lib/build/recipe/oauth2provider/types"; +/** + * 'export *' does not re-export a default. + * import NextJS from "supertokens-node/nextjs"; + * the above import statement won't be possible unless either + * - user add "esModuleInterop": true in their tsconfig.json file + * - we do the following change: + */ +import * as _default from "../../../lib/build/recipe/oauth2provider/types"; +export default _default; diff --git a/recipe/oauth2provider/types/index.js b/recipe/oauth2provider/types/index.js new file mode 100644 index 000000000..d508e2566 --- /dev/null +++ b/recipe/oauth2provider/types/index.js @@ -0,0 +1,6 @@ +"use strict"; +function __export(m) { + for (var p in m) if (!exports.hasOwnProperty(p)) exports[p] = m[p]; +} +exports.__esModule = true; +__export(require("../../../lib/build/recipe/oauth2provider/types")); diff --git a/test/config.test.js b/test/config.test.js index d81653329..7824a94ae 100644 --- a/test/config.test.js +++ b/test/config.test.js @@ -234,7 +234,7 @@ describe(`configTest: ${printPath("[test/config.test.js]")}`, function () { recipeList: [Session.init({ getTokenTransferMethod: () => "cookie" })], }); SessionRecipe.getInstanceOrThrowError(); - assert.strictEqual(SuperTokens.getInstanceOrThrowError().recipeModules.length, 3); // multitenancy&usermetadata is initialised by default + assert.strictEqual(SuperTokens.getInstanceOrThrowError().recipeModules.length, 4); // multitenancy&usermetadata&oauth2provider is initialised by default resetAll(); } @@ -252,7 +252,7 @@ describe(`configTest: ${printPath("[test/config.test.js]")}`, function () { }); SessionRecipe.getInstanceOrThrowError(); EmailPasswordRecipe.getInstanceOrThrowError(); - assert(SuperTokens.getInstanceOrThrowError().recipeModules.length === 4); // multitenancy&usermetadata is initialised by default + assert(SuperTokens.getInstanceOrThrowError().recipeModules.length === 5); // multitenancy&usermetadata&oauth2provider is initialised by default resetAll(); } }); diff --git a/test/jwt/getJWKS.test.js b/test/jwt/getJWKS.test.js index 5c4eab262..e9593cc12 100644 --- a/test/jwt/getJWKS.test.js +++ b/test/jwt/getJWKS.test.js @@ -118,9 +118,12 @@ describe(`getJWKS: ${printPath("[test/jwt/getJWKS.test.js]")}`, function () { assert(Object.keys(response).length === 1); assert(response.keys !== undefined); assert(response.keys.length > 0); - assert(headers["cache-control"].includes(", must-revalidate")); - assert(headers["cache-control"].includes("max-age=")); - assert(!headers["cache-control"].includes("max-age=60,")); + const cacheControlHeaderParts = headers["cache-control"].split(", "); + assert.strictEqual(cacheControlHeaderParts.length, 2); + assert(cacheControlHeaderParts[0].startsWith("max-age=60")); + const maxAge = Number.parseInt(cacheControlHeaderParts[0].split("=")[1]); + assert(maxAge >= 60); + assert.strictEqual(cacheControlHeaderParts[1], "must-revalidate"); }); it("Test that we can override the Cache-Control header through the function", async function () { diff --git a/test/oauth2/config.test.js b/test/oauth2/config.test.js new file mode 100644 index 000000000..5d2858925 --- /dev/null +++ b/test/oauth2/config.test.js @@ -0,0 +1,45 @@ +let assert = require("assert"); + +const { printPath, setupST, startST, killAllST, cleanST } = require("../utils"); +let { ProcessState } = require("../../lib/build/processState"); +let STExpress = require("../../"); +const OAuth2ProviderRecipe = require("../../lib/build/recipe/oauth2provider/recipe").default; +let { Querier } = require("../../lib/build/querier"); +const { maxVersion } = require("../../lib/build/utils"); + +describe(`configTest: ${printPath("[test/oauth2/config.test.js]")}`, function () { + beforeEach(async function () { + await killAllST(); + await setupST(); + ProcessState.getInstance().reset(); + }); + + after(async function () { + await killAllST(); + await cleanST(); + }); + + it("Test that the recipe initializes without a config obj", async function () { + const connectionURI = await startST(); + STExpress.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [OAuth2ProviderRecipe.init()], + }); + + // Only run for version >= 2.9 + let querier = Querier.getNewInstanceOrThrowError(undefined); + let apiVersion = await querier.getAPIVersion(); + if (maxVersion(apiVersion, "2.8") === "2.8") { + return; + } + + OAuth2ProviderRecipe.getInstanceOrThrowError(); + }); +}); diff --git a/test/oauth2/oauth2client.test.js b/test/oauth2/oauth2client.test.js new file mode 100644 index 000000000..cc2b701f4 --- /dev/null +++ b/test/oauth2/oauth2client.test.js @@ -0,0 +1,205 @@ +let assert = require("assert"); + +const { printPath, setupST, startST, killAllST, cleanST } = require("../utils"); +let { ProcessState } = require("../../lib/build/processState"); +let STExpress = require("../../"); +let OAuth2Recipe = require("../../recipe/oauth2provider"); + +describe(`OAuth2ClientTests: ${printPath("[test/oauth2/oauth2client.test.js]")}`, function () { + beforeEach(async function () { + await killAllST(); + await setupST(); + ProcessState.getInstance().reset(); + }); + + after(async function () { + await killAllST(); + await cleanST(); + }); + + it("should create an OAuth2Client instance with empty input", async function () { + const connectionURI = await startST(); + STExpress.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [OAuth2Recipe.init()], + }); + + const { client } = await OAuth2Recipe.createOAuth2Client({}); + + assert(client.clientId !== undefined); + assert(client.clientSecret !== undefined); + assert.strictEqual(client.scope, "offline_access offline openid"); + }); + + it("should create an OAuth2Client instance with custom input", async function () { + const connectionURI = await startST(); + STExpress.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [OAuth2Recipe.init()], + }); + + const { client } = await OAuth2Recipe.createOAuth2Client( + { + clientName: "client_name", + }, + {} + ); + + assert.strictEqual(client.clientName, "client_name"); + }); + + it("should update the OAuth2Client", async function () { + const connectionURI = await startST(); + STExpress.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [OAuth2Recipe.init()], + }); + + // Create a client + const { client } = await OAuth2Recipe.createOAuth2Client( + { + scope: "offline_access offline", + redirectUris: ["http://localhost:3000"], + }, + {} + ); + + assert.strictEqual(client.scope, "offline_access offline"); + assert.strictEqual(JSON.stringify(client.redirectUris), JSON.stringify(["http://localhost:3000"])); + assert.strictEqual(JSON.stringify(client.metadata), JSON.stringify({})); + + // Update the client + const { client: updatedClient } = await OAuth2Recipe.updateOAuth2Client( + { + clientId: client.clientId, + clientSecret: "new_client_secret", + scope: "offline_access", + redirectUris: null, + metadata: { a: 1, b: 2 }, + }, + {} + ); + + assert.strictEqual(updatedClient.clientSecret, "new_client_secret"); + assert.strictEqual(updatedClient.scope, "offline_access"); + assert.strictEqual(updatedClient.redirectUris, null); + assert.strictEqual(JSON.stringify(updatedClient.metadata), JSON.stringify({ a: 1, b: 2 })); + }); + + it("should delete the OAuth2Client", async function () { + const connectionURI = await startST(); + STExpress.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [OAuth2Recipe.init()], + }); + + // Create a client + const { client } = await OAuth2Recipe.createOAuth2Client({}); + + assert.strictEqual(client.scope, "offline_access offline openid"); + + // Delete the client + const { status } = await OAuth2Recipe.deleteOAuth2Client( + { + clientId: client.clientId, + }, + {} + ); + + assert.strictEqual(status, "OK"); + }); + + it("should get OAuth2Clients with pagination", async function () { + const connectionURI = await startST(); + STExpress.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [OAuth2Recipe.init()], + }); + + let clientIds = new Set(); + // Create 10 clients + for (let i = 0; i < 10; i++) { + const client = await OAuth2Recipe.createOAuth2Client({}); + clientIds.add(client.clientId); + } + + let allClients = []; + let nextPaginationToken = undefined; + + // Fetch clients in pages of 3 + do { + const result = await OAuth2Recipe.getOAuth2Clients( + { pageSize: 3, paginationToken: nextPaginationToken }, + {} + ); + assert.strictEqual(result.status, "OK"); + nextPaginationToken = result.nextPaginationToken; + allClients.push(...result.clients); + } while (nextPaginationToken); + + assert.strictEqual(allClients.length, 10); + // Check the client IDs + for (let i = 0; i < 10; i++) { + assert(clientIds.has(allClients[i].clientId)); + } + }); + + it("should get OAuth2Clients with filter", async function () { + const connectionURI = await startST(); + STExpress.init({ + supertokens: { + connectionURI, + }, + appInfo: { + apiDomain: "api.supertokens.io", + appName: "SuperTokens", + websiteDomain: "supertokens.io", + }, + recipeList: [OAuth2Recipe.init()], + }); + + // Create 5 clients with clientName = "customClientName" + for (let i = 0; i < 5; i++) { + await OAuth2Recipe.createOAuth2Client({ clientName: "customClientName" }); + } + + let result = await OAuth2Recipe.getOAuth2Clients({ clientName: "customClientName" }); + assert.strictEqual(result.status, "OK"); + assert.strictEqual(result.clients.length, 5); + }); +}); diff --git a/test/querier.test.js b/test/querier.test.js index 22c9646da..64ff73480 100644 --- a/test/querier.test.js +++ b/test/querier.test.js @@ -193,7 +193,7 @@ describe(`Querier: ${printPath("[test/querier.test.js]")}`, function () { 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/session/overwriteSessionDuringSignInUp.test.js b/test/session/overwriteSessionDuringSignInUp.test.js index 3bf12c781..dd397811a 100644 --- a/test/session/overwriteSessionDuringSignInUp.test.js +++ b/test/session/overwriteSessionDuringSignInUp.test.js @@ -119,7 +119,7 @@ describe(`overwriteSessionDuringSignInUp config: ${printPath( ); cookies = extractInfoFromResponse(res); - assert(cookies.accessTokenFromAny === undefined); + assert.notStrictEqual(cookies.accessTokenFromAny, undefined); }); it("test false", async function () { diff --git a/test/test-server/src/accountlinking.ts b/test/test-server/src/accountlinking.ts index 280e47ccc..6763deaac 100644 --- a/test/test-server/src/accountlinking.ts +++ b/test/test-server/src/accountlinking.ts @@ -4,7 +4,7 @@ import AccountLinkingRecipe from "../../../lib/build/recipe/accountlinking/recip import AccountLinking from "../../../recipe/accountlinking"; import * as supertokens from "../../../lib/build"; import { logger } from "./logger"; -import { serializeUser } from "./utils"; +import { serializeResponse, serializeUser } from "./utils"; const namespace = "com.supertokens:node-test-server:accountlinking"; const { logDebugMessage } = logger(namespace); @@ -15,7 +15,7 @@ const router = Router() logDebugMessage("AccountLinking:createPrimaryUser %j", req.body); const recipeUserId = supertokens.convertToRecipeUserId(req.body.recipeUserId); const response = await AccountLinking.createPrimaryUser(recipeUserId, req.body.userContext); - res.json({ ...response, ...serializeUser(response) }); + await serializeResponse(req, res, response); } catch (e) { next(e); } @@ -29,10 +29,7 @@ const router = Router() req.body.primaryUserId, req.body.userContext ); - res.json({ - ...response, - ...serializeUser(response), - }); + await serializeResponse(req, res, response); } catch (e) { next(e); } diff --git a/test/test-server/src/emailpassword.ts b/test/test-server/src/emailpassword.ts index 633c4bfd6..7770898f4 100644 --- a/test/test-server/src/emailpassword.ts +++ b/test/test-server/src/emailpassword.ts @@ -1,6 +1,6 @@ import { Router } from "express"; import EmailPassword from "../../../recipe/emailpassword"; -import { convertRequestSessionToSessionObject, serializeRecipeUserId, serializeUser } from "./utils"; +import { convertRequestSessionToSessionObject, serializeRecipeUserId, serializeResponse, serializeUser } from "./utils"; import * as supertokens from "../../../lib/build"; import { logger } from "./logger"; @@ -19,11 +19,7 @@ const router = Router() session, req.body.userContext ); - res.json({ - ...response, - ...serializeUser(response), - ...serializeRecipeUserId(response), - }); + await serializeResponse(req, res, response); } catch (e) { next(e); } @@ -39,11 +35,7 @@ const router = Router() session, req.body.userContext ); - res.json({ - ...response, - ...serializeUser(response), - ...serializeRecipeUserId(response), - }); + await serializeResponse(req, res, response); } catch (e) { next(e); } diff --git a/test/test-server/src/index.ts b/test/test-server/src/index.ts index 0eaa50a56..018d8a7d5 100644 --- a/test/test-server/src/index.ts +++ b/test/test-server/src/index.ts @@ -19,6 +19,11 @@ import ThirdPartyRecipe from "../../../lib/build/recipe/thirdparty/recipe"; import { TypeInput as ThirdPartyTypeInput } from "../../../lib/build/recipe/thirdparty/types"; import { TypeInput as MFATypeInput } from "../../../lib/build/recipe/multifactorauth/types"; import TOTPRecipe from "../../../lib/build/recipe/totp/recipe"; +import OAuth2ProviderRecipe from "../../../lib/build/recipe/oauth2provider/recipe"; +import { TypeInput as OAuth2ProviderTypeInput } from "../../../lib/build/recipe/oauth2provider/types"; +import OAuth2ClientRecipe from "../../../lib/build/recipe/oauth2client/recipe"; +import { TypeInput as OAuth2ClientTypeInput } from "../../../lib/build/recipe/oauth2client/types"; +import { TypeInput as OpenIdRecipeTypeInput } from "../../../lib/build/recipe/openid/types"; import UserMetadataRecipe from "../../../lib/build/recipe/usermetadata/recipe"; import SuperTokensRecipe from "../../../lib/build/supertokens"; import { RecipeListFunction } from "../../../lib/build/types"; @@ -32,6 +37,8 @@ import Session from "../../../recipe/session"; import { verifySession } from "../../../recipe/session/framework/express"; import ThirdParty from "../../../recipe/thirdparty"; import TOTP from "../../../recipe/totp"; +import OAuth2Provider from "../../../recipe/oauth2provider"; +import OAuth2Client from "../../../recipe/oauth2client"; import accountlinkingRoutes from "./accountlinking"; import emailpasswordRoutes from "./emailpassword"; import emailverificationRoutes from "./emailverification"; @@ -39,6 +46,7 @@ import { logger } from "./logger"; import multiFactorAuthRoutes from "./multifactorauth"; import multitenancyRoutes from "./multitenancy"; import passwordlessRoutes from "./passwordless"; +import OAuth2ProviderRoutes from "./oauth2provider"; import sessionRoutes from "./session"; import supertokensRoutes from "./supertokens"; import thirdPartyRoutes from "./thirdparty"; @@ -84,6 +92,8 @@ function STReset() { ProcessState.getInstance().reset(); MultiFactorAuthRecipe.reset(); TOTPRecipe.reset(); + OAuth2ProviderRecipe.reset(); + OAuth2ClientRecipe.reset(); SuperTokensRecipe.reset(); } @@ -279,15 +289,48 @@ function initST(config: any) { TOTP.init({ ...config, override: { - apis: overrideBuilderWithLogging("Multitenancy.override.apis", config?.override?.apis), - functions: overrideBuilderWithLogging( - "Multitenancy.override.functions", - config?.override?.functions - ), + apis: overrideBuilderWithLogging("TOTP.override.apis", config?.override?.apis), + functions: overrideBuilderWithLogging("TOTP.override.functions", config?.override?.functions), }, }) ); } + if (recipe.recipeId === "oauth2provider") { + let initConfig: OAuth2ProviderTypeInput = { + ...config, + }; + if (initConfig.override?.functions) { + initConfig.override = { + ...initConfig.override, + functions: getFunc(`${initConfig.override.functions}`), + }; + } + if (initConfig.override?.apis) { + initConfig.override = { + ...initConfig.override, + apis: getFunc(`${initConfig.override.apis}`), + }; + } + recipeList.push(OAuth2Provider.init(initConfig)); + } + if (recipe.recipeId === "oauth2client") { + let initConfig: OAuth2ClientTypeInput = { + ...config, + }; + if (initConfig.override?.functions) { + initConfig.override = { + ...initConfig.override, + functions: getFunc(`${initConfig.override.functions}`), + }; + } + if (initConfig.override?.apis) { + initConfig.override = { + ...initConfig.override, + apis: getFunc(`${initConfig.override.apis}`), + }; + } + recipeList.push(OAuth2Client.init(initConfig)); + } }); init.recipeList = recipeList; @@ -379,6 +422,7 @@ app.use("/test/multifactorauth", multiFactorAuthRoutes); app.use("/test/thirdparty", thirdPartyRoutes); app.use("/test/totp", TOTPRoutes); app.use("/test/usermetadata", userMetadataRoutes); +app.use("/test/oauth2provider", OAuth2ProviderRoutes); // *** Custom routes to help with session tests *** app.post("/create", async (req, res, next) => { diff --git a/test/test-server/src/oauth2provider.ts b/test/test-server/src/oauth2provider.ts new file mode 100644 index 000000000..1da664e3c --- /dev/null +++ b/test/test-server/src/oauth2provider.ts @@ -0,0 +1,75 @@ +import { Router } from "express"; +import OAuth2Provider from "../../../recipe/oauth2provider"; +import { logger } from "./logger"; + +const namespace = "com.supertokens:node-test-server:oauth2provider"; +const { logDebugMessage } = logger(namespace); + +const router = Router() + .post("/getoauth2clients", async (req, res, next) => { + try { + logDebugMessage("OAuth2Provider:getOAuth2Clients %j", req.body); + const response = await OAuth2Provider.getOAuth2Clients(req.body.input, req.body.userContext); + res.json(response); + } catch (e) { + next(e); + } + }) + .post("/createoauth2client", async (req, res, next) => { + try { + logDebugMessage("OAuth2Provider:createOAuth2Client %j", req.body); + const response = await OAuth2Provider.createOAuth2Client(req.body.input, req.body.userContext); + res.json(response); + } catch (e) { + next(e); + } + }) + .post("/updateoauth2client", async (req, res, next) => { + try { + logDebugMessage("OAuth2Provider:updateOAuth2Client %j", req.body); + const response = await OAuth2Provider.updateOAuth2Client(req.body.input, req.body.userContext); + res.json(response); + } catch (e) { + next(e); + } + }) + .post("/deleteoauth2client", async (req, res, next) => { + try { + logDebugMessage("OAuth2Provider:deleteOAuth2Client %j", req.body); + const response = await OAuth2Provider.deleteOAuth2Client(req.body.input, req.body.userContext); + res.json(response); + } catch (e) { + next(e); + } + }) + .post("/validateoauth2accesstoken", async (req, res, next) => { + try { + logDebugMessage("OAuth2Provider:validateOAuth2AccessToken %j", req.body); + const response = await OAuth2Provider.validateOAuth2AccessToken( + req.body.token, + req.body.requirements, + req.body.checkDatabase, + req.body.userContext + ); + res.json(response); + } catch (e) { + next(e); + } + }) + .post("/createtokenforclientcredentials", async (req, res, next) => { + try { + logDebugMessage("OAuth2Provider:createTokenForClientCredentials %j", req.body); + const response = await OAuth2Provider.createTokenForClientCredentials( + req.body.clientId, + req.body.clientSecret, + req.body.scope, + req.body.audience, + req.body.userContext + ); + res.json(response); + } catch (e) { + next(e); + } + }); + +export default router; diff --git a/test/test-server/src/passwordless.ts b/test/test-server/src/passwordless.ts index cc8c7fd99..6815eb2b9 100644 --- a/test/test-server/src/passwordless.ts +++ b/test/test-server/src/passwordless.ts @@ -1,7 +1,7 @@ import { Router } from "express"; import SuperTokens from "../../.."; import Passwordless from "../../../recipe/passwordless"; -import { convertRequestSessionToSessionObject, serializeRecipeUserId, serializeUser } from "./utils"; +import { convertRequestSessionToSessionObject, serializeRecipeUserId, serializeResponse, serializeUser } from "./utils"; import { logger } from "./logger"; const namespace = "com.supertokens:node-test-server:passwordless"; @@ -23,11 +23,7 @@ const router = Router() session: req.body.session && (await convertRequestSessionToSessionObject(req.body.session)), userContext: req.body.userContext, }); - res.json({ - ...response, - ...serializeUser(response), - ...serializeRecipeUserId(response), - }); + await serializeResponse(req, res, response); } catch (e) { next(e); } @@ -60,11 +56,7 @@ const router = Router() session: req.body.session && (await convertRequestSessionToSessionObject(req.body.session)), userContext: req.body.userContext, }); - res.json({ - ...response, - ...serializeUser(response), - ...serializeRecipeUserId(response), - }); + await serializeResponse(req, res, response); } catch (e) { next(e); } @@ -78,11 +70,7 @@ const router = Router() phoneNumber: req.body.phoneNumber, userContext: req.body.userContext, }); - res.json({ - ...response, - ...serializeUser(response), - ...serializeRecipeUserId(response), - }); + await serializeResponse(req, res, response); } catch (e) { next(e); } diff --git a/test/test-server/src/session.ts b/test/test-server/src/session.ts index a6f6ac9c3..49c2eab59 100644 --- a/test/test-server/src/session.ts +++ b/test/test-server/src/session.ts @@ -4,7 +4,7 @@ import * as supertokens from "../../../lib/build"; import SessionRecipe from "../../../lib/build/recipe/session/recipe"; import { logger } from "./logger"; import { getFunc } from "./testFunctionMapper"; -import { convertRequestSessionToSessionObject, deserializeClaim, deserializeValidator } from "./utils"; +import { convertRequestSessionToSessionObject, deserializeClaim, deserializeValidator, maxVersion } from "./utils"; import { logOverrideEvent } from "./overrideLogging"; const namespace = "com.supertokens:node-test-server:session"; @@ -12,9 +12,20 @@ const { logDebugMessage } = logger(namespace); const router = Router() .post("/createnewsessionwithoutrequestresponse", async (req, res, next) => { + const fdiVersion = req.headers["fdi-version"] as string; + try { logDebugMessage("Session.createNewSessionWithoutRequestResponse %j", req.body); - const recipeUserId = supertokens.convertToRecipeUserId(req.body.recipeUserId); + let recipeUserId; + if ( + maxVersion("1.17", fdiVersion) === "1.17" || + (maxVersion("2.0", fdiVersion) === fdiVersion && maxVersion("3.0", fdiVersion) !== fdiVersion) + ) { + // fdiVersion <= "1.17" || (fdiVersion >= "2.0" && fdiVersion < "3.0") + recipeUserId = supertokens.convertToRecipeUserId(req.body.userId); + } else { + recipeUserId = supertokens.convertToRecipeUserId(req.body.recipeUserId); + } const response = await Session.createNewSessionWithoutRequestResponse( req.body.tenantId || "public", recipeUserId, diff --git a/test/test-server/src/thirdparty.ts b/test/test-server/src/thirdparty.ts index a1e574356..53fcdfbaa 100644 --- a/test/test-server/src/thirdparty.ts +++ b/test/test-server/src/thirdparty.ts @@ -1,6 +1,6 @@ import { Router } from "express"; import ThirdParty from "../../../recipe/thirdparty"; -import { convertRequestSessionToSessionObject, serializeRecipeUserId, serializeUser } from "./utils"; +import { convertRequestSessionToSessionObject, serializeRecipeUserId, serializeResponse, serializeUser } from "./utils"; import { logger } from "./logger"; const namespace = "com.supertokens:node-test-server:thirdparty"; @@ -19,11 +19,7 @@ const router = Router().post("/manuallycreateorupdateuser", async (req, res, nex session, req.body.userContext ); - res.json({ - ...response, - ...serializeUser(response), - ...serializeRecipeUserId(response), - }); + await serializeResponse(req, res, response); } catch (e) { next(e); } diff --git a/test/test-server/src/utils.ts b/test/test-server/src/utils.ts index 69ecb21ce..3830825a5 100644 --- a/test/test-server/src/utils.ts +++ b/test/test-server/src/utils.ts @@ -137,7 +137,36 @@ export async function convertRequestSessionToSessionObject( return tokens; } -export function serializeUser(response) { +export async function serializeResponse(req, res, response) { + const fdiVersion: string = req.headers["fdi-version"] as string; + + await res.json({ + ...response, + ...serializeUser(response, fdiVersion), + ...serializeRecipeUserId(response, fdiVersion), + }); +} + +export function serializeUser(response, fdiVersion: string) { + // fdiVersion <= "1.17" || (fdiVersion >= "2.0" && fdiVersion < "3.0") + if ( + maxVersion("1.17", fdiVersion) === "1.17" || + (maxVersion("2.0", fdiVersion) === fdiVersion && maxVersion("3.0", fdiVersion) !== fdiVersion) + ) { + return { + ...("user" in response && response.user instanceof supertokens.User + ? { + user: { + id: (response.user as supertokens.User).id, + email: (response.user as supertokens.User).emails[0], + timeJoined: (response.user as supertokens.User).timeJoined, + tenantIds: (response.user as supertokens.User).tenantIds, + }, + } + : {}), + }; + } + return { ...("user" in response && response.user instanceof supertokens.User ? { @@ -147,7 +176,14 @@ export function serializeUser(response) { }; } -export function serializeRecipeUserId(response) { +export function serializeRecipeUserId(response, fdiVersion: string) { + if ( + maxVersion("1.17", fdiVersion) === "1.17" || + (maxVersion("2.0", fdiVersion) === fdiVersion && maxVersion("3.0", fdiVersion) !== fdiVersion) + ) { + // fdiVersion <= "1.17" || (fdiVersion >= "2.0" && fdiVersion < "3.0") + return {}; + } return { ...("recipeUserId" in response && response.recipeUserId instanceof supertokens.RecipeUserId ? { @@ -166,3 +202,22 @@ function popOrUseVal(arrOrValue: T | T[]): T { } return arrOrValue; } + +export function maxVersion(version1: string, version2: string): string { + let splittedv1 = version1.split("."); + let splittedv2 = version2.split("."); + let minLength = Math.min(splittedv1.length, splittedv2.length); + for (let i = 0; i < minLength; i++) { + let v1 = Number(splittedv1[i]); + let v2 = Number(splittedv2[i]); + if (v1 > v2) { + return version1; + } else if (v2 > v1) { + return version2; + } + } + if (splittedv1.length >= splittedv2.length) { + return version1; + } + return version2; +} diff --git a/test/utils.js b/test/utils.js index ff359390d..f679a6c3a 100644 --- a/test/utils.js +++ b/test/utils.js @@ -31,6 +31,7 @@ let PasswordlessRecipe = require("..//lib/build/recipe/passwordless/recipe").def 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; +const OAuth2Recipe = require("../lib/build/recipe/oauth2provider/recipe").default; let { ProcessState } = require("../lib/build/processState"); let { Querier } = require("../lib/build/querier"); let { maxVersion } = require("../lib/build/utils"); @@ -266,6 +267,7 @@ module.exports.resetAll = function (disableLogging = true) { MultitenancyRecipe.reset(); TotpRecipe.reset(); MultiFactorAuthRecipe.reset(); + OAuth2Recipe.reset(); if (disableLogging) { debug.disable(); } diff --git a/test/with-typescript/index.ts b/test/with-typescript/index.ts index f16582249..ae0b9fcd8 100644 --- a/test/with-typescript/index.ts +++ b/test/with-typescript/index.ts @@ -1356,6 +1356,7 @@ EmailPassword.init({ password, tenantId: input.tenantId, session: input.session, + shouldTryLinkingWithSessionUser: false, userContext: input.userContext, }); if (response.status === "WRONG_CREDENTIALS_ERROR") { @@ -1593,6 +1594,14 @@ Session.init({ getOpenIdDiscoveryConfiguration: async (input) => ({ issuer: "your issuer", jwks_uri: "https://your.api.domain/auth/jwt/jwks.json", + token_endpoint: "http://localhost:3000/auth/oauth2/token", + authorization_endpoint: "http://localhost:3000/auth/oauth2/auth", + userinfo_endpoint: "http://localhost:3000/auth/oauth2/userinfo", + revocation_endpoint: "http://localhost:3000/auth/oauth2/revoke", + token_introspection_endpoint: "http://localhost:3000/auth/oauth2/introspect", + id_token_signing_alg_values_supported: [], + response_types_supported: [], + subject_types_supported: [], status: "OK", }), }),