diff --git a/.circleci/config_continue.yml b/.circleci/config_continue.yml index 49dcb27fda..7a2e18b78c 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 0000000000..984a0b79bf --- /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 0000000000..a4a563b0ba --- /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 258b0a6d5b..83b0f82010 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 a5e771d2cd..b8649d610b 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 51d786670a..f7dfac511c 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 e3689decc4..731db467e3 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 44e400a633..555ac7ec54 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 0390f9a2d8..54d8bffe2f 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 83c69ac427..26f5069712 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 1bf6e5c3f0..b36f984163 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 8eea4a71a8..95fc4db80e 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 2ccabb278f..f594b381b7 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 19749d08cc..4aab539f77 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 4a175eaa90..b3a4647762 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 5e579c72e7..a51d4bfb3b 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 b49c296141..bd5c0994a5 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 dcc0a93353..5b8523ffe6 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 675226b497..f874813afc 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 c67ad58ed8..eb27b0dd83 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 739fa12f1d..25befe559c 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 4790215198..0fa6a3ca44 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 32a6126673..06aa48d568 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 4a06fc25c6..f49b73fb67 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 406e848b93..9266c8230c 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 a0505df2a0..e1e17acc9b 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 0a51291413..70aa42b9cc 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 aa517aeb91..0ceaaff1b1 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 b33d5ea905..1b768f60ec 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 9b8f1c5968..a8d5e9fe63 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 07e0f592d7..b419ca8f5e 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 924806d832..67fff866bf 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 16fc29c171..40c4e59f6b 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 424ec0684b..47136250e1 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 6b0023ef94..263f666cf2 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 2631d67d0c..2026f82677 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 3fbe074805..075154af82 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 b21f3b022a..7b6739e27d 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 268770d55b..97e81553d7 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 01e9a9c1a2..4b2dde8300 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 1aa3a99226..14add52170 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 8451f91563..b8195e3ab3 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 fba986690f..38df48a229 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 dc57c68f8e..1960f9e272 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 29bd8beb17..7ea37707f1 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 5f0d467ee6..8c5157ab9a 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 9ffe5d8a8f..57ccc03409 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 b8ff29461d..b49958d51f 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 b5f1d1bf74..a4448fbe8b 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 ca6422badf..47d66b7d6c 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 109d8a6298..cbb8597a19 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 c925842b14..cbdd87f7b7 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 9e714eb91a..98ff4ad41a 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 a0afc304a5..c340c79304 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 0000000000..88153ee4e2 --- /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 0000000000..0adeefc154 --- /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 fafdcdb206..00c5fe6c3e 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 afc940e7a1..edaa6e780d 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 dbc232eea3..bc25a61fed 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 e90a3d875b..ee366ca910 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 68e7fbbb52..f65eeeaee8 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 0bf9c36d09..5a850c2a0b 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 0e29dd06f5..e9eeeb3633 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 0369d066d2..7586e9f9b0 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 f5b6c69d5e..91278ce5f8 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 6ed81771b0..8e5e65db92 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 0ddb015d14..42a7f16b36 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 0987b15fb7..6a793ec2cf 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 7fcbacf41a..2c77224c0d 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 8c09609c13..acbbb1c7f6 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 58be906764..ee247490ef 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 e52174f38c..cbef7fd169 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 073f14b882..0ac8ddf6f4 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 9952b0124c..07976ada7e 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 45fe7885c6..da46bd6b1f 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 2ccd42009a..044c7e3194 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 0000000000..dd40e7025c --- /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 0000000000..159016c573 --- /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 0000000000..72cd6e46bd --- /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 0000000000..52631ac60a --- /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 0000000000..1fb91e7600 --- /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 0000000000..4f42a6cb47 --- /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 0000000000..fd09feb410 --- /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 0000000000..c70a69f412 --- /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 0000000000..1802271698 --- /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 0000000000..daf6b07f3c --- /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 0000000000..24599a2c7b --- /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 0000000000..e935c9e605 --- /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 0000000000..36b6858376 --- /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 0000000000..9f12373197 --- /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 0000000000..6a930e6416 --- /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 0000000000..0c5e2f39b3 --- /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 0000000000..f61f7c4f80 --- /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 0000000000..4e9c91e414 --- /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 0000000000..059876918e --- /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 0000000000..a064f9bb4f --- /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 0000000000..0218549fa4 --- /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 0000000000..1ba2baf4d4 --- /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 0000000000..3d2972c0d9 --- /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 0000000000..6e36c9c684 --- /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 0000000000..6f4253cef9 --- /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 0000000000..6ca65a5e32 --- /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 0000000000..5368582634 --- /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 0000000000..15b9da8088 --- /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 0000000000..902e734d56 --- /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 0000000000..46fadd8aa0 --- /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 0000000000..c697b77445 --- /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 0000000000..3ff27158cb --- /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 0000000000..d0b8cdf4e9 --- /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 0000000000..cfa8f704be --- /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 0000000000..cff72a21ad --- /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 0000000000..762963794e --- /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 0000000000..10dd838516 --- /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 0000000000..df8d4441c6 --- /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 0000000000..7a6f3c8f39 --- /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 0000000000..3a769700e1 --- /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 0000000000..25c12cc578 --- /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 0000000000..1b1cc2f501 --- /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 0000000000..3b6fee065a --- /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 0000000000..8428130ce8 --- /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 0000000000..7f439c767d --- /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 0000000000..a098ca1d7b --- /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 0000000000..4025b1b44c --- /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 0000000000..435b4e471b --- /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 b308bfffb1..0255426874 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 e94fd00929..e326447158 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 fd7a673984..4e0eb29e11 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 d4698099c2..be9ecbb29e 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 0edb22162d..58c278f4fb 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 5b907a8d51..0908e5b2c9 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 fefe0a19bf..7179971cf1 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 06b05ceb51..13617a2627 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 f0bcdc3ab5..0936acc2cf 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 419a905ac3..131c04f890 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 74774d3686..285ccb9ed3 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 a18ca5fc24..641ed598b6 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 15e4e9238d..2ec207ff3f 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 2535a78f6c..434f7902e3 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 3f0efd2cf9..37249db052 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 4aa6e6b057..84d5b0225a 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 55fbe988b8..fb87a07d1b 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 0489b1e818..d6259116f6 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 77afe511a3..b7307ae088 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 9c1969e847..df95edba8c 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 39c6017913..143db88452 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 2e522a2c86..750831f0a4 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 7204931a1a..3002f3bd25 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 1d33897a71..ea6c2819f8 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 e8f0af0b11..5011474a8a 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 e688b95ae8..0188b26dd1 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 027fcc4d7f..772fd4bcd4 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 6e8aadbe41..2ee060ce9e 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 27588fc51b..6c5a60d23c 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 3aead6ffc3..ddd2b7a000 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 a656dd8950..1f3c86029d 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 bce0eeaf4f..defa0739cc 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 3e54592c7e..7a7078b512 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 e09a4d2bc8..05e4531820 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 df183ed5c2..4b2590693c 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 0e8ee80f1b..dce4ef1e66 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 041656d627..8d0198e3b2 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 5465f884c3..5ad44874bf 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 e3aa25a5f2..2fd78898bd 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 53f5d56b6e..c341f1c199 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 ca12893f96..47dbdc1bad 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 778cd14ee6..e277192a97 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 7b3eef8893..75c3219aeb 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 7ee945c3bb..2da1ac7bb9 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 0000000000..84517d5e28 --- /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 0000000000..83cc3d405d --- /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 4ee6701754..c45e6de400 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 0659b1aaea..8743e57674 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 741fda01f5..30bbed8eaf 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 46c0c5e1af..ba4e3898a8 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 17148bd6b4..0d9902d49c 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 0443941bf0..5515639341 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 0000000000..9da2b8b076 --- /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 7e24b9f3b0..5bac2548fc 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 8cedfdbcb5..895239ca9b 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 995a341e04..e878130e21 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 d7f5a33ceb..b57b9329ae 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 18c936daf1..83e72e604e 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 4c0870ac35..96585bfc4f 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 843e270cde..2e35c5edf9 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 77c1161ffc..2899715cf6 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 ec9a2781d7..67a4b49e1c 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 e654d16b40..d5b071b51b 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 4c2e39d70b..1c883021ce 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 ee8b481dfb..563c95d4f3 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 4308c7cf1f..03bcab7ec0 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 dc8656124c..fa937c881f 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 2d7fd36a00..70dd5ed501 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 48316c97e9..d3694647a7 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 0000000000..7e18629e0b --- /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 0000000000..6e89436f98 --- /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 0000000000..8e45f0567d --- /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 0000000000..d2b2e2a02c --- /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 0000000000..7da475c5c5 --- /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 0000000000..a5115c8150 --- /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 0000000000..740d00d16c --- /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 0000000000..5996c2490d --- /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 0000000000..7105a9262f --- /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 0000000000..c6fc92e10e --- /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 0000000000..6745cec180 --- /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 0000000000..47b4be69cf --- /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 0000000000..9284c1e29f --- /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 0000000000..2c13ddad00 --- /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 0000000000..9974f77958 --- /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 0000000000..0ed290282c --- /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 0000000000..3e661068dd --- /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 0000000000..c062a7fb76 --- /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 0000000000..19c7173e3e --- /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 0000000000..01424b0519 --- /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 0000000000..861d40f689 --- /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 0000000000..538377ed86 --- /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 0000000000..6784056d57 --- /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 0000000000..d59cd45378 --- /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 90c291574a..169589677c 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 ee1f83e210..72921dffd4 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 1dc802de58..1726f9c433 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 f161e8d230..ce91ede5ee 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 c303cb0ca1..51702f4546 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 38aa06c4ba..39229cca76 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 af46ff86d9..246742c8c7 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 7b7894bb81..c769ad6ea2 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 cb946546aa..1f1abe48da 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 92b15e4930..43d00fd2a4 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 9410bfb004..5ec867387e 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 d157ce3b00..df4ad34c45 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 317d809ef7..cbbf6f95f3 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 c9f5bc6376..8c47ef6f6f 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 7a2a78498f..e7a5c296ac 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 d73ad8ccf6..57a7b42bec 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 64c526d032..0e8e670408 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 f02c4030e5..dc05e4469d 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 de4c5b3bb7..c6cb641581 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 f43570f77b..3361523527 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 e91bb2dc3d..889ba36856 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 f50813fe8d..cc57dff1d2 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 2dc3566f01..697bf96aac 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 56cc8073f9..6d5ace5e59 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 6e24fc0b69..ba7226836e 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 f556eba71c..e1f218143d 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 5aa79976fb..c179b0269c 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 cb60db8d3a..083cf821f6 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 0b6a2244ea..a82280d32a 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 f9a602e3c9..6225009bdb 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 21c7608da9..71e9847d31 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 745f23cf2b..a87a35c395 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 51ab77ea57..2ab9d24e2d 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 0f0176c7b3..36befb4f2a 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 eaee083f71..7141a8bfe9 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 3b73692c81..1f3c14b457 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 78109d1d0c..9f5df3e0bc 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 0000000000..54a2688884 --- /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 a90d96f0f7..e87e6b2a7d 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 b09011bf9c..144fd04a94 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 f7a917b2c1..22817ea601 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 967c8fbae4..238781e13c 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 ff2178e7ea..fa879d3ecd 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 0000000000..89f4241f83 --- /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 0000000000..f1b31d6dbd --- /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 0000000000..e475d45760 --- /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 0000000000..01b5c40c6e --- /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 0000000000..72d8d7e7d0 --- /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 0000000000..1b9fa10309 --- /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 0000000000..bdfe9d82c9 --- /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 0000000000..d508e25663 --- /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 d81653329b..7824a94ae9 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 5c4eab262c..e9593cc12f 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 0000000000..5d28589251 --- /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 0000000000..cc2b701f4f --- /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 22c9646da2..64ff734800 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 3bf12c781a..dd397811aa 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 280e47ccc2..6763deaac1 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 633c4bfd66..7770898f4c 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 0eaa50a560..018d8a7d58 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 0000000000..1da664e3c6 --- /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 cc8c7fd996..6815eb2b91 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 a6f6ac9c3a..49c2eab59a 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 a1e5743569..53fcdfbaa1 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 69ecb21ce6..3830825a50 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 ff359390d2..f679a6c3ab 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 f16582249d..ae0b9fcd89 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", }), }),